Compare commits

...

15 Commits

Author SHA1 Message Date
bitspill c18c81138a
Merge 0e24fffde0 into 19518a0844 2024-02-10 18:55:20 +01:00
diamondburned 19518a0844
cmdroute: Implement Router.{With,Group}
This commit implements cmdroute.Router.Group and cmdroute.Router.With,
similar to go-chi's Mux.

Fixes #418
2024-02-06 23:45:48 -08:00
diamondburned dbc4ae8978
state: Fix Messages() being wasteful on later calls 2024-01-22 02:41:10 -08:00
Benbebop 10d3626429
api: Add ID param to CreateCommandData (#417) 2024-01-06 19:17:03 -08:00
diamondburned ff26fded59
gateway: Run go generate for new event 2024-01-06 19:13:59 -08:00
Benbebop 3bbdae9bc1 discord: Add AttachmentOption to UnknownCommandOption 2024-01-06 00:47:34 -08:00
diamondburned 966d966897
state: Add MemberRoles and SortedRoles helpers 2024-01-02 21:06:59 -08:00
diamondburned 1782301df9
discord: Add SortRolesByPosition helper function 2024-01-02 21:06:47 -08:00
diamondburned 796b798f6a
discord: Add comment for CalcOverrides 2024-01-02 20:59:53 -08:00
diamondburned f7f228a848
gateway: Add undocumented ConversationSummaryUpdateEvent 2024-01-02 01:35:16 -08:00
diamondburned dffb6400fe
discord: Add ThreadMetadata.CreateTimestamp field 2024-01-01 16:40:31 -08:00
diamondburned 9809321f6f
handler: Add AllCallersForType for low-level control
This is needed in more silly main thread applications to prevent mutex
deadlocks from uncontrollable recursions.
2023-12-27 19:20:10 -08:00
dependabot[bot] fce16ffb87 build(deps): bump golang.org/x/crypto in /0-examples/voice
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.1.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.1.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 16:35:28 -08:00
dependabot[bot] 12ddfba102 build(deps): bump golang.org/x/crypto from 0.1.0 to 0.17.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.1.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.1.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 16:34:58 -08:00
bitspill 0e24fffde0
fix IntegerOptionType Unmarshall 2023-02-01 05:25:39 -06:00
16 changed files with 335 additions and 96 deletions

View File

@ -10,7 +10,8 @@ require (
require (
github.com/gorilla/schema v1.2.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
)

View File

@ -1,38 +1,53 @@
github.com/diamondburned/arikawa/v3 v3.0.0-rc.6 h1:lALIGxvojmcO/T6zeO53OzFXsuVWhXZ0Q/lYPehiKg0=
github.com/diamondburned/arikawa/v3 v3.0.0-rc.6/go.mod h1:5jBSNnp82Z/EhsKa6Wk9FsOqSxfVkNZDTDBPOj47LpY=
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647 h1:TJWvffl1cMLzSOvw8Wv3CQicuU9NaKDKXvBfh5T9W00=
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647/go.mod h1:xEJuvlmPx1wBKUWkx+MUp1ULSMQwSM9FS+bnFJhPQkk=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211001092434-39dca1131b70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -20,7 +20,9 @@ func (c *Client) CurrentApplication() (*discord.Application, error) {
}
// https://discord.com/developers/docs/interactions/application-commands#create-global-application-command
// https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands
type CreateCommandData struct {
ID discord.CommandID `json:"id,omitempty"`
Name string `json:"name"`
NameLocalizations discord.StringLocales `json:"name_localizations,omitempty"`
Description string `json:"description"`

View File

@ -10,9 +10,10 @@ import (
// Router is a router for slash commands. A zero-value Router is a valid router.
type Router struct {
nodes map[string]routeNode
mws []Middleware
stack []*Router
nodes map[string]routeNode
mws []Middleware
parent *Router // parent router, if any
groups []Router // next routers to check, if any
}
type routeNode interface {
@ -44,9 +45,6 @@ func NewRouter() *Router {
}
func (r *Router) init() {
if r.stack == nil {
r.stack = []*Router{r}
}
if r.nodes == nil {
r.nodes = make(map[string]routeNode, 4)
}
@ -75,7 +73,7 @@ func (r *Router) Use(mws ...Middleware) {
// parent command of the given name.
func (r *Router) Sub(name string, f func(r *Router)) {
sub := NewRouter()
sub.stack = append(append([]*Router(nil), r.stack...), sub)
sub.parent = r
f(sub)
r.add(name, routeNodeSub{sub})
@ -92,6 +90,39 @@ func (r *Router) AddFunc(name string, f CommandHandlerFunc) {
r.Add(name, f)
}
// Group creates a subrouter that handles certain commands within the parent
// command. This is useful for assigning middlewares to a group of commands that
// belong to the same parent command.
//
// For example, consider the following:
//
// r := cmdroute.NewRouter()
// r.Group(func(r *cmdroute.Router) {
// r.Use(cmdroute.Deferrable(client, cmdroute.DeferOpts{}))
// r.Add("foo", handleFoo)
// })
// r.Add("bar", handleBar)
//
// In this example, the middleware is only applied to the "foo" command, and not
// the "bar" command.
func (r *Router) Group(f func(r *Router)) {
f(r.With())
}
// With is similar to Group, but it returns a new router instead of calling a
// function with a new router. This is useful for chaining middlewares once,
// such as:
//
// r := cmdroute.NewRouter()
// r.With(cmdroute.Deferrable(client, cmdroute.DeferOpts{})).Add("foo", handleFoo)
func (r *Router) With(mws ...Middleware) *Router {
r.groups = append(r.groups, Router{})
sub := &r.groups[len(r.groups)-1]
sub.parent = r
sub.mws = append(sub.mws, mws...)
return sub
}
// HandleInteraction implements webhook.InteractionHandler. It only handles
// events of type CommandInteraction, otherwise nil is returned.
func (r *Router) HandleInteraction(ev *discord.InteractionEvent) *api.InteractionResponse {
@ -113,11 +144,11 @@ func (r *Router) callHandler(ev *discord.InteractionEvent, fn InteractionHandler
// Apply middlewares, parent last, first one added last. This ensures that
// when we call the handler, the middlewares are applied in the order they
// were added.
for i := len(r.stack) - 1; i >= 0; i-- {
r := r.stack[i]
for j := len(r.mws) - 1; j >= 0; j-- {
h = r.mws[j](h)
for r != nil {
for i := len(r.mws) - 1; i >= 0; i-- {
h = r.mws[i](h)
}
r = r.parent
}
return h.HandleInteraction(context.Background(), ev)
@ -162,7 +193,27 @@ type handlerData struct {
data discord.CommandInteractionOption
}
// findCommandHandler finds the command handler for the given command name.
// It checks the current router and its groups.
func (r *Router) findCommandHandler(ev *discord.InteractionEvent, data discord.CommandInteractionOption) (handlerData, bool) {
found, ok := r.findCommandHandlerOnce(ev, data)
if ok {
return found, true
}
for _, sub := range r.groups {
found, ok = sub.findCommandHandlerOnce(ev, data)
if ok {
return found, true
}
}
return handlerData{}, false
}
// findCommandHandlerOnce finds the command handler for the given command name.
// It only checks the current router and not its groups.
func (r *Router) findCommandHandlerOnce(ev *discord.InteractionEvent, data discord.CommandInteractionOption) (handlerData, bool) {
node, ok := r.nodes[data.Name]
if !ok {
return handlerData{}, false

View File

@ -165,34 +165,45 @@ func TestRouter(t *testing.T) {
})
t.Run("middlewares", func(t *testing.T) {
var stack []string
pushStack := func(s string) Middleware {
return func(next InteractionHandler) InteractionHandler {
return InteractionHandlerFunc(func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
stack = append(stack, s)
return next.HandleInteraction(ctx, ev)
})
}
}
var stack middlewareStacker
r := NewRouter()
r.Use(pushStack("root1"))
r.Use(pushStack("root2"))
r.Use(stack.pusher("root1"))
r.Use(stack.pusher("root2"))
r.Sub("test", func(r *Router) {
r.Use(pushStack("sub1.1"))
r.Use(pushStack("sub1.2"))
r.Sub("sub1", func(r *Router) {
r.Use(pushStack("sub2.1"))
r.Use(pushStack("sub2.2"))
r.Add("sub2", assertHandler(t, mockOptions))
// We put test 1 at the start, but test 2 at the end.
// The order should be preserved.
r.Use(stack.pusher("test.1"))
// unused
r.Group(func(r *Router) {
r.Use(stack.pusher("test.3"))
})
// unused
r.With(stack.pusher("test.4"))
r.Group(func(r *Router) {
r.Use(stack.pusher("test.5"))
r.Sub("sub", func(r *Router) {
r.Use(stack.pusher("test.sub.1"))
r.Use(stack.pusher("test.sub.2"))
r.Add("sub2", assertHandler(t, mockOptions))
})
})
// Test 2 goes here.
r.Use(stack.pusher("test.2"))
})
r.HandleInteraction(newInteractionEvent(&discord.CommandInteraction{
ID: 4,
Name: "test",
Options: []discord.CommandInteractionOption{
{
Name: "sub1",
Name: "sub",
Type: discord.SubcommandGroupOptionType,
Options: []discord.CommandInteractionOption{
{
@ -205,23 +216,15 @@ func TestRouter(t *testing.T) {
},
}))
expects := []string{
stack.expect(t, []string{
"root1",
"root2",
"sub1.1",
"sub1.2",
"sub2.1",
"sub2.2",
}
if len(stack) != len(expects) {
t.Fatalf("expected stack to have %d elements, got %d", len(expects), len(stack))
}
for i := range expects {
if stack[i] != expects[i] {
t.Fatalf("expected stack[%d] to be %q, got %q", i, expects[i], stack[i])
}
}
"test.1",
"test.2",
"test.5",
"test.sub.1",
"test.sub.2",
})
})
t.Run("deferred", func(t *testing.T) {
@ -335,6 +338,7 @@ var mockOptions = []discord.CommandInteractionOption{
},
}
// assertHandler asserts that the given options are equal to the expected options.
func assertHandler(t *testing.T, opts discord.CommandInteractionOptions) CommandHandler {
return CommandHandlerFunc(func(ctx context.Context, data CommandData) *api.InteractionResponseData {
if len(data.Options) != len(opts) {
@ -410,3 +414,25 @@ func strInteractionResp(resp *api.InteractionResponse) string {
}
return fmt.Sprintf("%d:%#v", resp.Type, resp.Data)
}
type middlewareStacker []string
func (m *middlewareStacker) pusher(s string) Middleware {
return func(next InteractionHandler) InteractionHandler {
return InteractionHandlerFunc(func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
*m = append(*m, s)
return next.HandleInteraction(ctx, ev)
})
}
}
func (m middlewareStacker) expect(t *testing.T, expects []string) {
if len(m) != len(expects) {
t.Fatalf("expected stack to have %d elements, got %d: %v", len(expects), len(m), m)
}
for i := range expects {
if m[i] != expects[i] {
t.Fatalf("expected stack[%d] to be %q, got %q", i, expects[i], m[i])
}
}
}

View File

@ -310,6 +310,9 @@ type ThreadMetadata struct {
// Invitable specifies whether non-moderators can add other
// non-moderators to a thread; only available on private threads.
Invitable bool `json:"invitable,omitempty"`
// CreateTimestamp is the timestamp when the thread was created; only
// populated for threads created after 2022-01-09.
CreateTimestamp *Timestamp `json:"thread_metadata,omitempty"`
}
type ThreadMember struct {

View File

@ -280,6 +280,8 @@ func (u *UnknownCommandOption) UnmarshalJSON(b []byte) error {
u.data = &MentionableOption{}
case NumberOptionType:
u.data = &NumberOption{}
case AttachmentOptionType:
u.data = &AttachmentOption{}
default:
// Copy the blob of bytes into a new slice.
u.raw = append(json.Raw(nil), b...)

View File

@ -1,6 +1,9 @@
package discord
import "time"
import (
"sort"
"time"
)
// https://discord.com/developers/docs/resources/guild#guild-object
type Guild struct {
@ -363,6 +366,15 @@ func (r Role) IconURLWithType(t ImageType) string {
return "https://cdn.discordapp.com/role-icons/" + r.ID.String() + "/" + t.format(r.Icon)
}
// SortRolesByPosition sorts the roles by their position.
// Roles with a higher position will be first in the slice, similar to how
// Discord sorts roles in the client.
func SortRolesByPosition(roles []Role) {
sort.Slice(roles, func(i, j int) bool {
return roles[i].Position > roles[j].Position
})
}
// https://discord.com/developers/docs/resources/guild#guild-member-object
//
// The field user won't be included in the member object attached to

View File

@ -540,16 +540,16 @@ var optionSupportedSnowflakeTypes = map[reflect.Type]CommandOptionType{
}
var optionKindMap = map[reflect.Kind]CommandOptionType{
reflect.Int: NumberOptionType,
reflect.Int8: NumberOptionType,
reflect.Int16: NumberOptionType,
reflect.Int32: NumberOptionType,
reflect.Int64: NumberOptionType,
reflect.Uint: NumberOptionType,
reflect.Uint8: NumberOptionType,
reflect.Uint16: NumberOptionType,
reflect.Uint32: NumberOptionType,
reflect.Uint64: NumberOptionType,
reflect.Int: IntegerOptionType,
reflect.Int8: IntegerOptionType,
reflect.Int16: IntegerOptionType,
reflect.Int32: IntegerOptionType,
reflect.Int64: IntegerOptionType,
reflect.Uint: IntegerOptionType,
reflect.Uint8: IntegerOptionType,
reflect.Uint16: IntegerOptionType,
reflect.Uint32: IntegerOptionType,
reflect.Uint64: IntegerOptionType,
reflect.Float32: NumberOptionType,
reflect.Float64: NumberOptionType,
reflect.String: StringOptionType,

View File

@ -170,6 +170,8 @@ func (p Permissions) Add(perm Permissions) Permissions {
return p | perm
}
// CalcOverrides calculates the permissions for a member in the given channel.
// Most of the time, you should use state.State.Permissions instead.
func CalcOverrides(
guild Guild, channel Channel, member Member, roles []Role) Permissions {

View File

@ -68,6 +68,7 @@ func init() {
func() ws.Event { return new(UserNoteUpdateEvent) },
func() ws.Event { return new(RelationshipAddEvent) },
func() ws.Event { return new(RelationshipRemoveEvent) },
func() ws.Event { return new(ConversationSummaryUpdateEvent) },
func() ws.Event { return new(ReadyEvent) },
func() ws.Event { return new(ReadySupplementalEvent) },
func() ws.Event { return new(GuildScheduledEventCreateEvent) },
@ -453,6 +454,12 @@ func (*RelationshipRemoveEvent) Op() ws.OpCode { return dispatchOp }
// EventType implements Event.
func (*RelationshipRemoveEvent) EventType() ws.EventType { return "RELATIONSHIP_REMOVE" }
// Op implements Event. It always returns 0.
func (*ConversationSummaryUpdateEvent) Op() ws.OpCode { return dispatchOp }
// EventType implements Event.
func (*ConversationSummaryUpdateEvent) EventType() ws.EventType { return "CONVERSATION_SUMMARY_UPDATE" }
// Op implements Event. It always returns 0.
func (*ReadyEvent) Op() ws.OpCode { return dispatchOp }

View File

@ -667,6 +667,36 @@ type RelationshipRemoveEvent struct {
discord.Relationship
}
// ConversationSummaryUpdateEvent is a dispatch event. It is undocumented.
type ConversationSummaryUpdateEvent struct {
ChannelID discord.ChannelID `json:"channel_id"`
GuildID discord.GuildID `json:"guild_id,omitempty"`
Summaries []ConversationSummary `json:"summaries"`
}
// ConversationSummary is a structure for ConversationSummaryUpdateEvent.
// It is undocumented.
type ConversationSummary struct {
Unsafe bool `json:"unsafe"`
// Topic is the topic of the conversation.
Topic string `json:"topic"`
// ShortSummary is a short summary of the conversation.
// It is in sentence form.
ShortSummary string `json:"summ_short"`
// People is a list of user IDs in the conversation.
People []discord.UserID `json:"people"`
// StartID is the ID of the first message in the conversation.
StartID discord.MessageID `json:"start_id"`
// EndID is the ID of the last message in the conversation.
EndID discord.MessageID `json:"end_id"`
// MessageIDs is a list of message IDs in the conversation.
MessageIDs []discord.MessageID `json:"message_ids"`
// ID is some kind of ID that identifies the conversation?
ID discord.Snowflake `json:"id"`
// Count is the number of messages in the conversation.
Count int `json:"count"`
}
// ReadyEvent is a dispatch event for READY.
//
// https://discord.com/developers/docs/topics/gateway#ready

2
go.mod
View File

@ -5,6 +5,6 @@ go 1.16
require (
github.com/gorilla/schema v1.2.0
github.com/gorilla/websocket v1.4.2
golang.org/x/crypto v0.1.0
golang.org/x/crypto v0.17.0
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
)

24
go.sum
View File

@ -5,32 +5,42 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -280,6 +280,46 @@ func MemberColor(m *discord.Member, role func(discord.RoleID) *discord.Role) (di
return c, pos != -1
}
// SortedRoles returns a list of roles sorted by their position.
// The roles are fetched using (*State).Roles.
func (s *State) SortedRoles(guildID discord.GuildID) ([]discord.Role, error) {
roles, err := s.Roles(guildID)
if err != nil {
return nil, err
}
discord.SortRolesByPosition(roles)
return roles, nil
}
// MemberRoles returns a list of roles that the given member has.
// The returned roles are sorted by their position.
func (s *State) MemberRoles(guildID discord.GuildID, userID discord.UserID) ([]discord.Role, error) {
roles, err := s.Roles(guildID)
if err != nil {
return nil, fmt.Errorf("failed to get roles for guild: %w", err)
}
member, err := s.Member(guildID, userID)
if err != nil {
return nil, fmt.Errorf("failed to get member: %w", err)
}
filtered := make([]discord.Role, 0, len(member.RoleIDs))
roleSearch:
for _, roleID := range member.RoleIDs {
for _, role := range roles {
if role.ID == roleID {
filtered = append(filtered, role)
continue roleSearch
}
}
return nil, fmt.Errorf("failed to find role %d for member", roleID)
}
discord.SortRolesByPosition(filtered)
return filtered, nil
}
////
// Permissions gets the user's permissions in the given channel. If the channel
@ -667,17 +707,25 @@ func (s *State) Messages(channelID discord.ChannelID, limit uint) ([]discord.Mes
return storeMessages[:limit], nil
}
// Decrease the limit, if we aren't fetching all messages.
if limit > 0 {
limit -= uint(len(storeMessages))
fetchLimit := limit
// If the user is requesting less than MaxMessages, then increase the limit
// to at least that so that channels don't accidentally get marked as tiny.
if fetchLimit < uint(s.MaxMessages()) {
fetchLimit = uint(s.MaxMessages())
}
var before discord.MessageID = 0
// Decrease the fetchLimit, if we aren't fetching all messages.
if fetchLimit > 0 {
fetchLimit -= uint(len(storeMessages))
}
var before discord.MessageID
if len(storeMessages) > 0 {
before = storeMessages[len(storeMessages)-1].ID
}
apiMessages, err := s.Session.MessagesBefore(channelID, before, limit)
apiMessages, err := s.Session.MessagesBefore(channelID, before, fetchLimit)
if err != nil {
return nil, err
}
@ -690,6 +738,9 @@ func (s *State) Messages(channelID discord.ChannelID, limit uint) ([]discord.Mes
}
if len(apiMessages) == 0 {
if limit > 0 && len(storeMessages) > int(limit) {
return storeMessages[:limit], nil
}
return storeMessages, nil
}
@ -724,7 +775,12 @@ func (s *State) Messages(channelID discord.ChannelID, limit uint) ([]discord.Mes
}
}
return append(storeMessages, apiMessages...), nil
msgs := append(storeMessages, apiMessages...)
if limit > 0 && len(msgs) > int(limit) {
msgs = msgs[:limit]
}
return msgs, nil
}
////

View File

@ -38,32 +38,48 @@ func New() *Handler {
// Call calls all handlers with the given event. This is an internal method; use
// with care.
func (h *Handler) Call(ev interface{}) {
v := reflect.ValueOf(ev)
t := reflect.TypeOf(ev)
h.mutex.RLock()
defer h.mutex.RUnlock()
all := h.AllCallersForType(t)
all(func(caller Caller) bool {
caller.Call(v)
return true
})
}
typedHandlers := h.events[t].Entries
anyHandlers := h.events[nil].Entries
// AllCallersForType returns all callers for the given event type. This is an
// internal method that is rarely useful for external use and should be used
// with care.
func (h *Handler) AllCallersForType(t reflect.Type) func(yield func(Caller) bool) {
return func(yield func(Caller) bool) {
h.mutex.RLock()
defer h.mutex.RUnlock()
if len(typedHandlers) == 0 && len(anyHandlers) == 0 {
return
}
typedHandlers := h.events[t].Entries
anyHandlers := h.events[nil].Entries
v := reflect.ValueOf(ev)
for _, entry := range typedHandlers {
if entry.isInvalid() {
continue
if len(typedHandlers) == 0 && len(anyHandlers) == 0 {
return
}
entry.Call(v)
}
for _, entry := range anyHandlers {
if entry.isInvalid() || entry.not(t) {
continue
for _, entry := range typedHandlers {
if entry.isInvalid() {
continue
}
if !yield(entry) {
return
}
}
for _, entry := range anyHandlers {
if entry.isInvalid() || entry.not(t) {
continue
}
if !yield(entry) {
return
}
}
entry.Call(v)
}
}
@ -239,6 +255,10 @@ func (h *Handler) addHandler(fn interface{}, sync bool) (rm func(), err error) {
}, nil
}
// Caller is an interface that can be used to call a handler.
// It directly accepts a reflect.Value, which is the event.
type Caller interface{ Call(ev reflect.Value) }
type handler struct {
event reflect.Type // underlying type; arg0 or chan underlying type
callback reflect.Value
@ -248,6 +268,8 @@ type handler struct {
isOnce bool
}
var _ Caller = (*handler)(nil)
// newHandler reflects either a channel or a function into a handler. A function
// must only have a single argument being the event and no return, and a channel
// must have the event type as the underlying type.