diff --git a/.editorconfig b/.editorconfig index 5997b1dc3..cf6454ec8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -47,3 +47,6 @@ ij_go_wrap_func_result_newline_before_rparen = true indent_style = tab max_line_length = 600 ij_smart_tabs = true + +[{*.yaml,*.yml}] +indent_size = 2 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..988ea2779 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,35 @@ +# branch names +"type:bugfix": + - head-branch: [ '^fix/', '^bugfix/', '^hotfix/'] +"type:docs": + - head-branch: [ '^docs/', '^documentation/' ] +"type:enhancement": + - head-branch: [ '^feature/', '^feat/' ] +"type:refactor": + - head-branch: [ '^refactor/' ] + +# changed files +"t:caching": + - changed-files: + - any-glob-to-any-file: [ 'cache/*' ] +"t:gateway": + - changed-files: + - any-glob-to-any-file: [ 'gateway/*' ] +"t:handler": + - changed-files: + - any-glob-to-any-file: [ 'handler/*' ] +"t:oauth2": + - changed-files: + - any-glob-to-any-file: [ 'oauth2/*' ] +"t:ratelimits": + - changed-files: + - any-glob-to-any-file: [ 'rest/rest_rate_limiter_*', 'gateway/gateway_rate_limiter_*' ] +"t:rest": + - changed-files: + - any-glob-to-any-file: [ 'rest/*' ] +"t:sharding": + - changed-files: + - any-glob-to-any-file: [ 'sharding/*' ] +"t:voice": + - changed-files: + - any-glob-to-any-file: [ 'voice/*' ] \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 000000000..80891322c --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,48 @@ +name: Go + +on: + push: + paths: + - '**/*.go' + - '**/go.mod' + - '**/go.sum' + - '.github/workflows/go.yml' + pull_request_target: + paths: + - '**/*.go' + - '**/go.mod' + - '**/go.sum' + - '.github/workflows/go.yml' + +jobs: + gobuild: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.21 + - uses: actions/checkout@v3 + - name: go build + run: go build -v ./... + + gotest: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.21 + - uses: actions/checkout@v3 + - name: go build + run: go test -v ./... + + golangci: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.21 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..8b00a2c3a --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,12 @@ +name: "Label pull request" +on: + - pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 345ea9c17..000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Go - -on: [ push ] - -jobs: - gobuild: - runs-on: ubuntu-latest - steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - uses: actions/checkout@v3 - - name: go build - run: go build -v ./... - - gotest: - runs-on: ubuntu-latest - steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - uses: actions/checkout@v3 - - name: go build - run: go test -v ./... - - golangci: - runs-on: ubuntu-latest - steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - uses: actions/checkout@v3 - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: latest diff --git a/README.md b/README.md index 571240c10..962920449 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ A full Ping Pong example can also be found [here](https://github.com/disgoorg/di ### Logging -DisGo uses our own small [logging interface](https://github.com/disgoorg/log) which you can use with most other logging libraries. This lib also comes with a default logger which is based on the standard log package. +DisGo uses [slog](https://pkg.go.dev/log/slog) for logging. ## Documentation @@ -156,10 +156,6 @@ Being used in production by FredBoat, Dyno, LewdBot, and more. Is a [Lavalink-Client](https://github.com/freyacodes/Lavalink) which can be used to communicate with Lavalink to play/search tracks -### [DisLog](https://github.com/disgoorg/dislog) - -Is a Discord webhook logger hook for [logrus](https://github.com/sirupsen/logrus) - ## Other Golang Discord Libraries * [discordgo](https://github.com/bwmarrin/discordgo) diff --git a/_examples/application_commands/gateway/example.go b/_examples/application_commands/gateway/example.go index fe7b6615c..e318e7ee8 100644 --- a/_examples/application_commands/gateway/example.go +++ b/_examples/application_commands/gateway/example.go @@ -2,17 +2,16 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -40,30 +39,29 @@ var ( ) func main() { - log.SetLevel(log.LevelInfo) - log.Info("starting example...") - log.Info("disgo version: ", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithDefaultGateway(), bot.WithEventListenerFunc(commandListener), ) if err != nil { - log.Fatal("error while building disgo instance: ", err) + slog.Error("error while building disgo instance", slog.Any("err", err)) return } defer client.Close(context.TODO()) if _, err = client.Rest().SetGuildCommands(client.ApplicationID(), guildID, commands); err != nil { - log.Fatal("error while registering commands: ", err) + slog.Error("error while registering commands", slog.Any("err", err)) } if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s @@ -78,7 +76,7 @@ func commandListener(event *events.ApplicationCommandInteractionCreate) { Build(), ) if err != nil { - event.Client().Logger().Error("error on sending response: ", err) + slog.Error("error on sending response", slog.Any("err", err)) } } } diff --git a/_examples/application_commands/http/example.go b/_examples/application_commands/http/example.go index 85b2f4d13..59e6fbc93 100644 --- a/_examples/application_commands/http/example.go +++ b/_examples/application_commands/http/example.go @@ -2,19 +2,18 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/oasisprotocol/curve25519-voi/primitives/ed25519" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/httpserver" + "github.com/disgoorg/snowflake/v2" + "github.com/oasisprotocol/curve25519-voi/primitives/ed25519" ) var ( @@ -43,9 +42,8 @@ var ( ) func main() { - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Info("disgo version: ", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) // use custom ed25519 verify implementation httpserver.Verify = func(publicKey httpserver.PublicKey, message, sig []byte) bool { @@ -60,21 +58,20 @@ func main() { bot.WithEventListenerFunc(commandListener), ) if err != nil { - log.Fatal("error while building disgo instance: ", err) - return + panic("error while building disgo instance: " + err.Error()) } defer client.Close(context.TODO()) if _, err = client.Rest().SetGuildCommands(client.ApplicationID(), guildID, commands); err != nil { - log.Fatal("error while registering commands: ", err) + panic("error while registering commands: " + err.Error()) } if err = client.OpenHTTPServer(); err != nil { - log.Fatal("error while starting http server: ", err) + panic("error while starting http server: " + err.Error()) } - log.Info("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s @@ -88,7 +85,7 @@ func commandListener(event *events.ApplicationCommandInteractionCreate) { SetEphemeral(data.Bool("ephemeral")). Build(), ); err != nil { - event.Client().Logger().Error("error on sending response: ", err) + event.Client().Logger().Error("error on sending response", slog.Any("err", err)) } } } diff --git a/_examples/application_commands/http/go.mod b/_examples/application_commands/http/go.mod index a58b722b4..14095cb5e 100644 --- a/_examples/application_commands/http/go.mod +++ b/_examples/application_commands/http/go.mod @@ -1,19 +1,19 @@ module github.com/disgoorg/disgo/_examples/application_commands/http -go 1.18 +go 1.21 + +replace github.com/disgoorg/disgo => ../../../ require ( - github.com/disgoorg/disgo v0.16.8 - github.com/disgoorg/log v1.2.1 - github.com/disgoorg/snowflake/v2 v2.0.1 - github.com/oasisprotocol/curve25519-voi v0.0.0-20230110094441-db37f07504ce + github.com/disgoorg/disgo v0.18.14 + github.com/disgoorg/snowflake/v2 v2.0.3 + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a ) require ( - github.com/disgoorg/json v1.1.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect - golang.org/x/crypto v0.12.0 // indirect - golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b // indirect - golang.org/x/sys v0.11.0 // indirect + github.com/disgoorg/json v1.2.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect ) diff --git a/_examples/application_commands/http/go.sum b/_examples/application_commands/http/go.sum index dfd5b37d7..9dd3abd74 100644 --- a/_examples/application_commands/http/go.sum +++ b/_examples/application_commands/http/go.sum @@ -1,24 +1,22 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/disgoorg/disgo v0.16.8 h1:tvUeX+3Iu8U6koDc8RAgcQadRciWJwsI95Y7edHqq2g= -github.com/disgoorg/disgo v0.16.8/go.mod h1:5fsaUpfu6Yv0p+PfmsAeQkV395KQskVu/d1bdq8vsNI= -github.com/disgoorg/json v1.1.0 h1:7xigHvomlVA9PQw9bMGO02PHGJJPqvX5AnwlYg/Tnys= -github.com/disgoorg/json v1.1.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= -github.com/disgoorg/log v1.2.1 h1:kZYAWkUBcGy4LbZcgYtgYu49xNVLy+xG5Uq3yz5VVQs= -github.com/disgoorg/log v1.2.1/go.mod h1:hhQWYTFTnIGzAuFPZyXJEi11IBm9wq+/TVZt/FEwX0o= -github.com/disgoorg/snowflake/v2 v2.0.1 h1:CuUxGLwggUxEswZOmZ+mZ5i0xSumQdXW9tXW7uGqe+0= -github.com/disgoorg/snowflake/v2 v2.0.1/go.mod h1:SPU9c2CNn5DSyb86QcKtdZgix9osEtKrHLW4rMhfLCs= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/oasisprotocol/curve25519-voi v0.0.0-20230110094441-db37f07504ce h1:/pEpMk55wH0X+E5zedGEMOdLuWmV8P4+4W3+LZaM6kg= -github.com/oasisprotocol/curve25519-voi v0.0.0-20230110094441-db37f07504ce/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disgoorg/json v1.2.0 h1:6e/j4BCfSHIvucG1cd7tJPAOp1RgnnMFSqkvZUtEd1Y= +github.com/disgoorg/json v1.2.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= +github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= +github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b h1:qYTY2tN72LhgDj2rtWG+LI6TXFl2ygFQQ4YezfVaGQE= -github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI= -golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/application_commands/localization/example.go b/_examples/application_commands/localization/example.go index 4a7d1d3b4..c8fdfa8a5 100644 --- a/_examples/application_commands/localization/example.go +++ b/_examples/application_commands/localization/example.go @@ -2,17 +2,16 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -64,30 +63,28 @@ var ( ) func main() { - log.SetLevel(log.LevelTrace) - log.Info("starting example...") - log.Infof("disgo version: %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithDefaultGateway(), bot.WithEventListenerFunc(commandListener), ) if err != nil { - log.Fatal("error while building disgo instance: ", err) - return + panic("error while building disgo instance: " + err.Error()) } defer client.Close(context.TODO()) if _, err = client.Rest().SetGuildCommands(client.ApplicationID(), guildID, commands); err != nil { - log.Fatal("error while registering commands: ", err) + panic("error while registering commands: " + err.Error()) } if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + panic("error while connecting to gateway: " + err.Error()) } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s @@ -102,7 +99,7 @@ func commandListener(event *events.ApplicationCommandInteractionCreate) { Build(), ) if err != nil { - event.Client().Logger().Error("error on sending response: ", err) + event.Client().Logger().Error("error on sending response", slog.Any("err", err)) } } } diff --git a/_examples/auto_moderation/example.go b/_examples/auto_moderation/example.go index c5e638b36..0e41558d9 100644 --- a/_examples/auto_moderation/example.go +++ b/_examples/auto_moderation/example.go @@ -3,20 +3,19 @@ package main import ( "context" "fmt" + "log/slog" "os" "os/signal" "syscall" "time" - "github.com/disgoorg/json" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/json" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -26,9 +25,8 @@ var ( ) func main() { - log.SetLevel(log.LevelInfo) - log.Info("starting example...") - log.Infof("disgo version: %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentAutoModerationConfiguration, gateway.IntentAutoModerationExecution)), @@ -49,16 +47,18 @@ func main() { }), ) if err != nil { - log.Fatal("error while building bot: ", err) + slog.Error("error while building bot", slog.Any("err", err)) + return } defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) + return } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s @@ -86,7 +86,7 @@ func showCaseAutoMod(client bot.Client) { Enabled: json.Ptr(true), }) if err != nil { - log.Error("error while creating rule: ", err) + slog.Error("error while creating rule", slog.Any("err", err)) return } @@ -107,7 +107,7 @@ func showCaseAutoMod(client bot.Client) { }, }) if err != nil { - log.Error("error while updating rule: ", err) + slog.Error("error while updating rule", slog.Any("err", err)) return } @@ -115,7 +115,7 @@ func showCaseAutoMod(client bot.Client) { err = client.Rest().DeleteAutoModerationRule(guildID, rule.ID) if err != nil { - log.Error("error while deleting rule: ", err) + slog.Error("error while deleting rule", slog.Any("err", err)) return } diff --git a/_examples/components/example.go b/_examples/components/example.go index 8e6b349ec..9c918892b 100644 --- a/_examples/components/example.go +++ b/_examples/components/example.go @@ -2,17 +2,15 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" - "github.com/disgoorg/disgo/events" - - "github.com/disgoorg/log" - "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" ) @@ -21,9 +19,8 @@ var ( ) func main() { - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Infof("disgo version: %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildMessages, gateway.IntentDirectMessages)), @@ -46,16 +43,17 @@ func main() { }), ) if err != nil { - log.Fatal("error while building bot: ", err) + slog.Error("error while building bot", slog.Any("err", err)) + return } - defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) + return } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/custom_cache/example.go b/_examples/custom_cache/example.go index d2f94c191..bf2ed19c9 100644 --- a/_examples/custom_cache/example.go +++ b/_examples/custom_cache/example.go @@ -2,20 +2,19 @@ package main import ( "context" + "log/slog" "os" "os/signal" "sync" "syscall" "time" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -23,9 +22,8 @@ var ( ) func main() { - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Infof("disgo version: %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuilds|gateway.IntentGuildMessages|gateway.IntentDirectMessages)), @@ -35,7 +33,8 @@ func main() { ), ) if err != nil { - log.Fatal("error while building bot: ", err) + slog.Error("error while building bot", slog.Any("err", err)) + return } defer func() { @@ -47,10 +46,10 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err = client.OpenGateway(ctx); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/echo/echo.go b/_examples/echo/echo.go index 05b792ca9..080d05b56 100644 --- a/_examples/echo/echo.go +++ b/_examples/echo/echo.go @@ -3,21 +3,19 @@ package main import ( "context" "errors" - "fmt" + "log/slog" "net" "os" "os/signal" "syscall" "time" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" "github.com/disgoorg/disgo/voice" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -27,9 +25,7 @@ var ( ) func main() { - log.SetLevel(log.LevelTrace) - log.SetFlags(log.LstdFlags | log.Llongfile) - log.Info("starting up") + slog.Info("starting up") client, err := disgo.New(token, bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuildVoiceStates)), @@ -38,16 +34,18 @@ func main() { }), ) if err != nil { - log.Fatal("error creating client: ", err) + slog.Error("error creating client", slog.Any("err", err)) + return } defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error connecting to voicegateway: ", err) + slog.Error("error connecting to voice gateway", slog.Any("err", err)) + return } - log.Info("ExampleBot is now running. Press CTRL-C to exit.") + slog.Info("ExampleBot is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) <-s @@ -68,7 +66,7 @@ func play(client bot.Client) { conn.Close(ctx2) }() - println("starting playback") + slog.Info("starting playback") if err := conn.SetSpeaking(ctx, voice.SpeakingFlagMicrophone); err != nil { panic("error setting speaking flag: " + err.Error()) @@ -81,18 +79,18 @@ func play(client bot.Client) { packet, err := conn.UDP().ReadPacket() if err != nil { if errors.Is(err, net.ErrClosed) { - println("connection closed") + slog.Info("connection closed") return } - fmt.Printf("error while reading from reader: %s", err) + slog.Info("error while reading from reader", slog.Any("err", err)) continue } if _, err = conn.UDP().Write(packet.Opus); err != nil { if errors.Is(err, net.ErrClosed) { - println("connection closed") + slog.Info("connection closed") return } - fmt.Printf("error while writing to UDPConn: %s", err) + slog.Info("error while writing to UDPConn", slog.Any("err", err)) continue } } diff --git a/_examples/guild_scheduled_events/example.go b/_examples/guild_scheduled_events/example.go index 27ca45e24..af3b5acf3 100644 --- a/_examples/guild_scheduled_events/example.go +++ b/_examples/guild_scheduled_events/example.go @@ -2,13 +2,12 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" "time" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/cache" @@ -22,9 +21,7 @@ var ( ) func main() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.SetLevel(log.LevelDebug) - log.Info("starting example...") + slog.Info("starting example...") client, err := disgo.New(token, bot.WithGatewayConfigOpts( @@ -36,22 +33,22 @@ func main() { bot.WithMemberChunkingFilter(bot.MemberChunkingFilterNone), bot.WithEventListeners(&events.ListenerAdapter{ OnGuildScheduledEventCreate: func(event *events.GuildScheduledEventCreate) { - log.Infof("%T\n", event) + slog.Info("OnGuildScheduledEventCreate") }, OnGuildScheduledEventUpdate: func(event *events.GuildScheduledEventUpdate) { - log.Infof("%T\n", event) + slog.Info("OnGuildScheduledEventUpdate") }, OnGuildScheduledEventDelete: func(event *events.GuildScheduledEventDelete) { - log.Infof("%T\n", event) + slog.Info("OnGuildScheduledEventDelete") }, OnGuildScheduledEventUserAdd: func(event *events.GuildScheduledEventUserAdd) { - log.Infof("%T\n", event) + slog.Info("OnGuildScheduledEventUserAdd") }, OnGuildScheduledEventUserRemove: func(event *events.GuildScheduledEventUserRemove) { - log.Infof("%T\n", event) + slog.Info("OnGuildScheduledEventUserRemove") }, OnMessageCreate: func(event *events.MessageCreate) { - log.Infof("%T\n", event) + slog.Info("OnMessageCreate") if event.Message.Content != "test" { return } @@ -60,7 +57,7 @@ func main() { Name: "test", PrivacyLevel: discord.ScheduledEventPrivacyLevelGuildOnly, ScheduledStartTime: time.Now().Add(time.Hour), - Description: "", + Description: "test", EntityType: discord.ScheduledEventEntityTypeVoice, }) @@ -68,7 +65,6 @@ func main() { gse, _ = event.Client().Rest().UpdateGuildScheduledEvent(gse.GuildID, gse.ID, discord.GuildScheduledEventUpdate{ Status: &status, }) - //_ = gse.AudioChannel().Connect() time.Sleep(time.Second * 10) @@ -76,20 +72,19 @@ func main() { gse, _ = event.Client().Rest().UpdateGuildScheduledEvent(gse.GuildID, gse.ID, discord.GuildScheduledEventUpdate{ Status: &status, }) - //_ = gse.Guilds().Disconnect() }, }), ) if err != nil { - log.Fatal("error while building bot instance: ", err) + slog.Error("error while building bot instance", slog.Any("err", err)) return } if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to discord: ", err) + slog.Error("error while connecting to discord", slog.Any("err", err)) } - log.Info("Example is now running. Press CTRL-C to exit.") + slog.Info("Example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/handler/example.go b/_examples/handler/example.go index 44c2a826c..630231b84 100644 --- a/_examples/handler/example.go +++ b/_examples/handler/example.go @@ -2,19 +2,17 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" - "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/handler" "github.com/disgoorg/disgo/handler/middleware" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -64,9 +62,8 @@ var ( ) func main() { - log.SetLevel(log.LevelInfo) - log.Info("starting example...") - log.Infof("disgo version: %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) r := handler.New() r.Use(middleware.Logger) @@ -83,7 +80,7 @@ func main() { r.Use(middleware.Print("group2")) r.Command("/ping", handlePing) r.Command("/ping2", handleContent("pong2")) - r.Component("button1/{data}", handleComponent) + r.Component("/button1/{data}", handleComponent) }) r.NotFound(handleNotFound) @@ -92,20 +89,22 @@ func main() { bot.WithEventListeners(r), ) if err != nil { - log.Fatal("error while building bot: ", err) + slog.Error("error while building bot", slog.Any("err", err)) + return } if err = handler.SyncCommands(client, commands, []snowflake.ID{guildID}); err != nil { - log.Fatal("error while syncing commands: ", err) + slog.Error("error while syncing commands", slog.Any("err", err)) + return } defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) } - log.Info("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s @@ -118,7 +117,7 @@ func handleContent(content string) handler.CommandHandler { } func handleVariableContent(event *handler.CommandEvent) error { - group := event.Variables["group"] + group := event.Vars["group"] return event.CreateMessage(discord.MessageCreate{Content: "group: " + group}) } @@ -127,17 +126,17 @@ func handlePing(event *handler.CommandEvent) error { Content: "pong", Components: []discord.ContainerComponent{ discord.ActionRowComponent{ - discord.NewPrimaryButton("button1", "button1/testData"), + discord.NewPrimaryButton("button1", "/button1/testData"), }, }, }) } func handleComponent(event *handler.ComponentEvent) error { - data := event.Variables["data"] + data := event.Vars["data"] return event.CreateMessage(discord.MessageCreate{Content: "component: " + data}) } -func handleNotFound(event *events.InteractionCreate) error { - return event.Respond(discord.InteractionResponseTypeCreateMessage, discord.MessageCreate{Content: "not found"}) +func handleNotFound(event *handler.InteractionEvent) error { + return event.CreateMessage(discord.MessageCreate{Content: "not found"}) } diff --git a/_examples/listening_events/example.go b/_examples/listening_events/example.go index 52bcd9e36..0d1102a48 100644 --- a/_examples/listening_events/example.go +++ b/_examples/listening_events/example.go @@ -2,17 +2,16 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -21,9 +20,8 @@ var ( ) func main() { - log.SetLevel(log.LevelInfo) - log.Info("starting example...") - log.Info("disgo version: ", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithDefaultGateway(), @@ -32,17 +30,17 @@ func main() { bot.WithEventListeners(&events.ListenerAdapter{OnMessageCreate: eventListenerFunc}), ) if err != nil { - log.Fatal("error while building disgo instance: ", err) + slog.Error("error while building disgo instance", slog.Any("err", err)) return } defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/message_collector/example.go b/_examples/message_collector/example.go index f9ffe3536..c5dd4698d 100644 --- a/_examples/message_collector/example.go +++ b/_examples/message_collector/example.go @@ -2,6 +2,7 @@ package main import ( "context" + "log/slog" "os" "os/signal" "strconv" @@ -10,9 +11,6 @@ import ( "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" - - "github.com/disgoorg/log" - "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" @@ -23,25 +21,26 @@ var ( ) func main() { - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Infof("disgo version: %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildMessages, gateway.IntentDirectMessages, gateway.IntentMessageContent)), bot.WithEventListenerFunc(onMessageCreate), ) if err != nil { - log.Fatal("error while building bot: ", err) + slog.Error("error while building bot", slog.Any("err", err)) + return } defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) + return } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/oauth2/example.go b/_examples/oauth2/example.go index 46111f755..184382870 100644 --- a/_examples/oauth2/example.go +++ b/_examples/oauth2/example.go @@ -2,19 +2,18 @@ package main import ( "fmt" + "log/slog" "math/rand" "net/http" "os" "time" - "github.com/disgoorg/json" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/oauth2" "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/json" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -22,7 +21,6 @@ var ( clientID = snowflake.GetEnv("client_id") clientSecret = os.Getenv("client_secret") baseURL = os.Getenv("base_url") - logger = log.Default() httpClient = http.DefaultClient client oauth2.Client sessions map[string]oauth2.Session @@ -33,11 +31,10 @@ func init() { } func main() { - logger.SetLevel(log.LevelDebug) - logger.Info("starting example...") - logger.Infof("disgo %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) - client = oauth2.New(clientID, clientSecret, oauth2.WithLogger(logger), oauth2.WithRestClientConfigOpts(rest.WithHTTPClient(httpClient))) + client = oauth2.New(clientID, clientSecret, oauth2.WithRestClientConfigOpts(rest.WithHTTPClient(httpClient))) mux := http.NewServeMux() mux.HandleFunc("/", handleRoot) @@ -90,7 +87,11 @@ func handleRoot(w http.ResponseWriter, r *http.Request) { } func handleLogin(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, client.GenerateAuthorizationURL(baseURL+"/trylogin", discord.PermissionsNone, 0, false, discord.OAuth2ScopeIdentify, discord.OAuth2ScopeGuilds, discord.OAuth2ScopeEmail, discord.OAuth2ScopeConnections, discord.OAuth2ScopeWebhookIncoming), http.StatusSeeOther) + params := oauth2.AuthorizationURLParams{ + RedirectURI: baseURL + "/trylogin", + Scopes: []discord.OAuth2Scope{discord.OAuth2ScopeIdentify, discord.OAuth2ScopeGuilds, discord.OAuth2ScopeEmail, discord.OAuth2ScopeConnections, discord.OAuth2ScopeWebhookIncoming}, + } + http.Redirect(w, r, client.GenerateAuthorizationURL(params), http.StatusSeeOther) } func handleTryLogin(w http.ResponseWriter, r *http.Request) { diff --git a/_examples/pagination/examplebot.go b/_examples/pagination/examplebot.go index 80a0c605f..b4efdce6f 100644 --- a/_examples/pagination/examplebot.go +++ b/_examples/pagination/examplebot.go @@ -1,11 +1,9 @@ package main import ( - _ "embed" + "log/slog" "os" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/rest" ) @@ -13,10 +11,8 @@ import ( var token = os.Getenv("disgo_token") func main() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Info("bot version: ", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client := rest.New(rest.NewClient(token)) @@ -25,15 +21,15 @@ func main() { var i int for page.Next() { for _, m := range page.Items { - println(m.ID) + slog.Info(m.ID.String()) } - println("---") + slog.Info("---") i++ if i >= 3 { break } } if page.Err != nil { - log.Error(page.Err) + slog.Error("error getting messages", slog.Any("err", page.Err)) } } diff --git a/_examples/ping_pong/example.go b/_examples/ping_pong/example.go index 0a89fc7fc..6435788cb 100644 --- a/_examples/ping_pong/example.go +++ b/_examples/ping_pong/example.go @@ -2,12 +2,11 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" @@ -16,8 +15,8 @@ import ( ) func main() { - log.SetLevel(log.LevelDebug) - log.SetFlags(log.LstdFlags | log.Lshortfile) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(os.Getenv("disgo_token"), bot.WithGatewayConfigOpts( @@ -29,16 +28,18 @@ func main() { bot.WithEventListenerFunc(onMessageCreate), ) if err != nil { - log.Fatal("error while building disgo: ", err) + slog.Error("error while building disgo", slog.Any("err", err)) + return } defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("errors while connecting to gateway: ", err) + slog.Error("errors while connecting to gateway", slog.Any("err", err)) + return } - log.Info("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/proxy/example.go b/_examples/proxy/example.go index 4dfd5810d..399fd68e0 100644 --- a/_examples/proxy/example.go +++ b/_examples/proxy/example.go @@ -2,13 +2,11 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" @@ -16,6 +14,7 @@ import ( "github.com/disgoorg/disgo/gateway" "github.com/disgoorg/disgo/rest" "github.com/disgoorg/disgo/sharding" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -45,9 +44,8 @@ var ( ) func main() { - log.SetLevel(log.LevelInfo) - log.Info("starting example...") - log.Info("disgo version: ", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithShardManagerConfigOpts( @@ -65,21 +63,23 @@ func main() { ) if err != nil { - log.Fatal("error while building disgo instance: ", err) + slog.Error("error while building disgo instance", slog.Any("err", err)) return } defer client.Close(context.TODO()) if _, err = client.Rest().SetGuildCommands(client.ApplicationID(), guildID, commands); err != nil { - log.Fatal("error while registering commands: ", err) + slog.Error("error while registering commands", slog.Any("err", err)) + return } if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) + return } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s @@ -94,7 +94,7 @@ func commandListener(event *events.ApplicationCommandInteractionCreate) { Build(), ) if err != nil { - event.Client().Logger().Error("error on sending response: ", err) + slog.Error("error on sending response", slog.Any("err", err)) } } } diff --git a/_examples/sharding/example.go b/_examples/sharding/example.go index 135c7b8cd..6c98706da 100644 --- a/_examples/sharding/example.go +++ b/_examples/sharding/example.go @@ -2,12 +2,11 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" @@ -21,10 +20,8 @@ var ( ) func main() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Info("disgo version: ", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.Any("version", disgo.Version)) client, err := disgo.New(token, bot.WithShardManagerConfigOpts( @@ -39,24 +36,26 @@ func main() { bot.WithEventListeners(&events.ListenerAdapter{ OnMessageCreate: onMessageCreate, OnGuildReady: func(event *events.GuildReady) { - log.Infof("guild %s ready", event.GuildID) + slog.Info("guild %s ready", event.GuildID) }, OnGuildsReady: func(event *events.GuildsReady) { - log.Infof("guilds on shard %d ready", event.ShardID) + slog.Info("guilds on shard %d ready", event.ShardID) }, }), ) if err != nil { - log.Fatalf("error while building disgo: %s", err) + slog.Error("error while building disgo", slog.Any("err", err)) + return } defer client.Close(context.TODO()) if err = client.OpenShardManager(context.TODO()); err != nil { - log.Fatal("error while connecting to gateway: ", err) + slog.Error("error while connecting to gateway", slog.Any("err", err)) + return } - log.Infof("example is now running. Press CTRL-C to exit.") + slog.Info("example is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/test/commands.go b/_examples/test/commands.go index e3a85ab01..dec675376 100644 --- a/_examples/test/commands.go +++ b/_examples/test/commands.go @@ -1,7 +1,7 @@ package main import ( - "github.com/disgoorg/log" + "log/slog" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" @@ -40,6 +40,6 @@ var commands = []discord.ApplicationCommandCreate{ func registerCommands(client bot.Client) { if _, err := client.Rest().SetGuildCommands(client.ApplicationID(), guildID, commands); err != nil { - log.Fatalf("error while registering guild commands: %s", err) + slog.Error("error while registering guild commands", slog.Any("err", err)) } } diff --git a/_examples/test/examplebot.go b/_examples/test/examplebot.go index cac003bed..0d72c9776 100644 --- a/_examples/test/examplebot.go +++ b/_examples/test/examplebot.go @@ -3,18 +3,17 @@ package main import ( "context" _ "embed" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -26,10 +25,8 @@ var ( ) func main() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Info("bot version: ", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.Any("version", disgo.Version)) client, err := disgo.New(token, bot.WithGatewayConfigOpts( @@ -43,19 +40,19 @@ func main() { bot.WithEventListeners(listener), ) if err != nil { - log.Fatal("error while building bot instance: ", err) + slog.Error("error while building bot instance", slog.Any("err", err)) return } registerCommands(client) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to discord: ", err) + slog.Error("error while connecting to discord", slog.Any("err", err)) } defer client.Close(context.TODO()) - log.Info("ExampleBot is now running. Press CTRL-C to exit.") + slog.Info("ExampleBot is now running. Press CTRL-C to exit.") s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s diff --git a/_examples/test/listeners.go b/_examples/test/listeners.go index 716b4bfbd..20d64ed01 100644 --- a/_examples/test/listeners.go +++ b/_examples/test/listeners.go @@ -2,11 +2,10 @@ package main import ( "bytes" + "log/slog" "strings" "time" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" @@ -48,7 +47,7 @@ func componentListener(event *events.ComponentInteractionCreate) { ids := strings.Split(data.CustomID(), ":") switch ids[0] { case "modal": - _ = event.CreateModal(discord.ModalCreate{ + _ = event.Modal(discord.ModalCreate{ CustomID: "test" + ids[1], Title: "Test" + ids[1] + " Modal", Components: []discord.ContainerComponent{ @@ -88,7 +87,7 @@ func componentListener(event *events.ComponentInteractionCreate) { switch data.CustomID() { case "test3": if err := event.DeferUpdateMessage(); err != nil { - log.Errorf("error sending interaction response: %s", err) + slog.Error("error sending interaction response", slog.Any("err", err)) } _, _ = event.Client().Rest().CreateFollowupMessage(event.ApplicationID(), event.Token(), discord.NewMessageCreateBuilder(). SetEphemeral(true). @@ -101,7 +100,7 @@ func componentListener(event *events.ComponentInteractionCreate) { switch data.CustomID() { case "test4": if err := event.DeferUpdateMessage(); err != nil { - log.Errorf("error sending interaction response: %s", err) + slog.Error("error sending interaction response", slog.Any("err", err)) } _, _ = event.Client().Rest().CreateFollowupMessage(event.ApplicationID(), event.Token(), discord.NewMessageCreateBuilder(). SetEphemeral(true). @@ -121,7 +120,7 @@ func applicationCommandListener(event *events.ApplicationCommandInteractionCreat Build(), ) if err != nil { - event.Client().Logger().Error("error on sending response: ", err) + slog.Error("error on sending response", slog.Any("err", err)) } case "say": @@ -159,7 +158,7 @@ func applicationCommandListener(event *events.ApplicationCommandInteractionCreat func autocompleteListener(event *events.AutocompleteInteractionCreate) { switch event.Data.CommandName { case "test2": - if err := event.Result([]discord.AutocompleteChoice{ + if err := event.AutocompleteResult([]discord.AutocompleteChoice{ discord.AutocompleteChoiceInt{ Name: "test1", Value: 1, @@ -169,7 +168,7 @@ func autocompleteListener(event *events.AutocompleteInteractionCreate) { Value: 2, }, }); err != nil { - event.Client().Logger().Error("error on sending response: ", err) + slog.Error("error on sending response", slog.Any("err", err)) } } } @@ -199,7 +198,7 @@ func messageListener(event *events.GuildMessageCreate) { Build(), ) if err != nil { - event.Client().Logger().Error("error on sending response: ", err) + slog.Error("error on sending response", slog.Any("err", err)) } time.Sleep(1 * time.Second) _, err = event.Client().Rest().UpdateMessage(event.ChannelID, message.ID, discord.NewMessageUpdateBuilder(). @@ -208,7 +207,7 @@ func messageListener(event *events.GuildMessageCreate) { Build(), ) if err != nil { - event.Client().Logger().Error("error on updating response: ", err) + slog.Error("error on updating response", slog.Any("err", err)) } case "panic": @@ -227,7 +226,7 @@ func messageListener(event *events.GuildMessageCreate) { go func() { message, err := event.Client().Rest().CreateMessage(event.ChannelID, discord.NewMessageCreateBuilder().SetContent("test").Build()) if err != nil { - log.Errorf("error while sending file: %s", err) + slog.Error("error while sending file", slog.Any("err", err)) return } time.Sleep(time.Second * 2) diff --git a/_examples/threads/example.go b/_examples/threads/example.go index 94052318a..c79bd5dc1 100644 --- a/_examples/threads/example.go +++ b/_examples/threads/example.go @@ -2,12 +2,11 @@ package main import ( "context" + "log/slog" "os" "os/signal" "syscall" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/cache" @@ -19,10 +18,8 @@ import ( var token = os.Getenv("token") func main() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.SetLevel(log.LevelInfo) - log.Info("starting example...") - log.Infof("bot version: %s", disgo.Version) + slog.Info("starting example...") + slog.Info("bot version", slog.String("version", disgo.Version)) client, err := disgo.New(token, bot.WithGatewayConfigOpts( @@ -36,45 +33,45 @@ func main() { OnMessageCreate: func(event *events.MessageCreate) { if channel, ok := event.Channel(); ok { if _, ok = channel.(discord.GuildThread); ok { - println("MessageCreateEvent") + slog.Info("MessageCreateEvent") } } }, OnThreadCreate: func(event *events.ThreadCreate) { - println("ThreadCreateEvent") + slog.Info("ThreadCreateEvent", slog.Any("newly_created", event.NewlyCreated)) }, OnThreadUpdate: func(event *events.ThreadUpdate) { - println("ThreadUpdateEvent") + slog.Info("ThreadUpdateEvent") }, OnThreadDelete: func(event *events.ThreadDelete) { - println("ThreadDeleteEvent") + slog.Info("ThreadDeleteEvent") }, OnThreadHide: func(event *events.ThreadHide) { - println("ThreadHideEvent") + slog.Info("ThreadHideEvent") }, OnThreadShow: func(event *events.ThreadShow) { - println("ThreadShowEvent") + slog.Info("ThreadShowEvent") }, OnThreadMemberAdd: func(event *events.ThreadMemberAdd) { - println("ThreadMemberAddEvent") + slog.Info("ThreadMemberAddEvent") }, OnThreadMemberUpdate: func(event *events.ThreadMemberUpdate) { - println("ThreadMemberUpdateEvent") + slog.Info("ThreadMemberUpdateEvent") }, OnThreadMemberRemove: func(event *events.ThreadMemberRemove) { - println("ThreadMemberRemoveEvent") + slog.Info("ThreadMemberRemoveEvent") }, }), ) if err != nil { - log.Fatal("error while building bot instance: ", err) + slog.Error("error while building bot instance", slog.Any("err", err)) return } defer client.Close(context.TODO()) if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error while connecting to discord: ", err) + slog.Error("error while connecting to discord", slog.Any("err", err)) } s := make(chan os.Signal, 1) diff --git a/_examples/verified_roles/main.go b/_examples/verified_roles/main.go index cd5096b2c..a053bd837 100644 --- a/_examples/verified_roles/main.go +++ b/_examples/verified_roles/main.go @@ -1,18 +1,17 @@ package main import ( + "log/slog" "math/rand" "net/http" "os" "strconv" - "github.com/disgoorg/json" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/oauth2" + "github.com/disgoorg/json" ) var ( @@ -25,14 +24,14 @@ var ( ) func main() { - log.SetLevel(log.LevelDebug) - log.Info("starting example...") - log.Infof("disgo %s", disgo.Version) + slog.Info("starting example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) var err error client, err = disgo.New(token) if err != nil { - log.Panic(err) + slog.Error("error creating client", slog.Any("err", err)) + return } _, _ = client.Rest().UpdateApplicationRoleConnectionMetadata(client.ApplicationID(), []discord.ApplicationRoleConnectionMetadata{ @@ -53,7 +52,11 @@ func main() { } func handleVerify(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, oAuth2Client.GenerateAuthorizationURL(baseURL+"/callback", discord.PermissionsNone, 0, false, discord.OAuth2ScopeIdentify, discord.OAuth2ScopeRoleConnectionsWrite), http.StatusTemporaryRedirect) + params := oauth2.AuthorizationURLParams{ + RedirectURI: baseURL + "/callback", + Scopes: []discord.OAuth2Scope{discord.OAuth2ScopeIdentify, discord.OAuth2ScopeRoleConnectionsWrite}, + } + http.Redirect(w, r, oAuth2Client.GenerateAuthorizationURL(params), http.StatusTemporaryRedirect) } func handleCallback(w http.ResponseWriter, r *http.Request) { diff --git a/_examples/voice/voice.go b/_examples/voice/voice.go index 6d316be74..90b71742a 100644 --- a/_examples/voice/voice.go +++ b/_examples/voice/voice.go @@ -4,21 +4,18 @@ import ( "context" "encoding/binary" "io" - _ "net/http/pprof" + "log/slog" "os" "os/signal" "syscall" "time" - "github.com/disgoorg/disgo/voice" - - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/disgo/voice" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -28,9 +25,8 @@ var ( ) func main() { - log.SetLevel(log.LevelInfo) - log.SetFlags(log.LstdFlags | log.Llongfile) - log.Info("starting up") + slog.Info("starting up") + slog.Info("disgo version", slog.String("version", disgo.Version)) s := make(chan os.Signal, 1) @@ -41,7 +37,7 @@ func main() { }), ) if err != nil { - log.Fatal("error creating client: ", err) + slog.Error("error creating client", slog.Any("err", err)) } defer func() { @@ -51,10 +47,11 @@ func main() { }() if err = client.OpenGateway(context.TODO()); err != nil { - log.Fatal("error connecting to gateway: ", err) + slog.Error("error connecting to gateway", slog.Any("error", err)) + return } - log.Info("ExampleBot is now running. Press CTRL-C to exit.") + slog.Info("ExampleBot is now running. Press CTRL-C to exit.") signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s } diff --git a/_examples/webhook/example.go b/_examples/webhook/example.go index 5720020a8..9cb1c3e5d 100644 --- a/_examples/webhook/example.go +++ b/_examples/webhook/example.go @@ -2,17 +2,16 @@ package main import ( "context" + "log/slog" "os" "sync" "time" - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/rest" "github.com/disgoorg/disgo/webhook" + "github.com/disgoorg/snowflake/v2" ) var ( @@ -21,10 +20,8 @@ var ( ) func main() { - log.SetLevel(log.LevelDebug) - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.Info("starting webhook example...") - log.Info("disgo version: ", disgo.Version) + slog.Info("starting webhook example...") + slog.Info("disgo version", slog.String("version", disgo.Version)) // construct new webhook client client := webhook.New(webhookID, webhookToken) @@ -41,7 +38,7 @@ func main() { // wait for all messages to be sent wg.Wait() - log.Info("exiting webhook example...") + slog.Info("exiting webhook example...") } // send(s) a message to the webhook @@ -54,6 +51,6 @@ func send(wg *sync.WaitGroup, client webhook.Client, i int) { // delay each request by 2 seconds rest.WithDelay(2*time.Second), ); err != nil { - log.Errorf("error sending message %d: %s", i, err) + slog.Error("error sending message to webhook", slog.Any("error", err), slog.Int("i", i)) } } diff --git a/bot/client.go b/bot/client.go index 3ef9a888d..eb5936512 100644 --- a/bot/client.go +++ b/bot/client.go @@ -2,9 +2,7 @@ package bot import ( "context" - - "github.com/disgoorg/log" - "github.com/disgoorg/snowflake/v2" + "log/slog" "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" @@ -13,6 +11,7 @@ import ( "github.com/disgoorg/disgo/rest" "github.com/disgoorg/disgo/sharding" "github.com/disgoorg/disgo/voice" + "github.com/disgoorg/snowflake/v2" ) var _ Client = (*clientImpl)(nil) @@ -22,7 +21,7 @@ var _ Client = (*clientImpl)(nil) // Create a new client with disgo.New. type Client interface { // Logger returns the logger for the client. - Logger() log.Logger + Logger() *slog.Logger // Close will clean up all disgo internals and close the discord gracefully. Close(ctx context.Context) @@ -93,6 +92,9 @@ type Client interface { // limit : The number of discord.Member(s) to return. RequestMembersWithQuery(ctx context.Context, guildID snowflake.ID, presence bool, nonce string, query string, limit int) error + // RequestSoundboardSounds a gateway.MessageDataRequestSoundboardSounds to the specific gateway.Gateway and requests the SoundboardSounds of the specified guilds. + RequestSoundboardSounds(ctx context.Context, guildIDs ...snowflake.ID) error + // SetPresence sends new presence data to the gateway.Gateway. SetPresence(ctx context.Context, opts ...gateway.PresenceOpt) error @@ -116,7 +118,7 @@ type clientImpl struct { token string applicationID snowflake.ID - logger log.Logger + logger *slog.Logger restServices rest.Rest @@ -134,7 +136,7 @@ type clientImpl struct { memberChunkingManager MemberChunkingManager } -func (c *clientImpl) Logger() log.Logger { +func (c *clientImpl) Logger() *slog.Logger { return c.logger } @@ -278,6 +280,15 @@ func (c *clientImpl) RequestMembersWithQuery(ctx context.Context, guildID snowfl }) } +func (c *clientImpl) RequestSoundboardSounds(ctx context.Context, guildIDs ...snowflake.ID) error { + if !c.HasGateway() { + return discord.ErrNoGateway + } + return c.gateway.Send(ctx, gateway.OpcodeRequestSoundboardSounds, gateway.MessageDataRequestSoundboardSounds{ + GuildIDs: guildIDs, + }) +} + func (c *clientImpl) SetPresence(ctx context.Context, opts ...gateway.PresenceOpt) error { if !c.HasGateway() { return discord.ErrNoGateway diff --git a/bot/config.go b/bot/config.go index a0575299f..880f182d3 100644 --- a/bot/config.go +++ b/bot/config.go @@ -2,8 +2,7 @@ package bot import ( "fmt" - - "github.com/disgoorg/log" + "log/slog" "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" @@ -18,7 +17,7 @@ import ( // DefaultConfig returns a Config with sensible defaults. func DefaultConfig(gatewayHandlers map[gateway.EventType]GatewayEventHandler, httpHandler HTTPServerEventHandler) *Config { return &Config{ - Logger: log.Default(), + Logger: slog.Default(), EventManagerConfigOpts: []EventManagerConfigOpt{WithGatewayHandlers(gatewayHandlers), WithHTTPServerHandler(httpHandler)}, MemberChunkingFilter: MemberChunkingFilterNone, } @@ -26,7 +25,7 @@ func DefaultConfig(gatewayHandlers map[gateway.EventType]GatewayEventHandler, ht // Config lets you configure your Client instance. type Config struct { - Logger log.Logger + Logger *slog.Logger RestClient rest.Client RestClientConfigOpts []rest.ConfigOpt @@ -65,8 +64,8 @@ func (c *Config) Apply(opts []ConfigOpt) { } } -// WithLogger lets you inject your own logger implementing log.Logger. -func WithLogger(logger log.Logger) ConfigOpt { +// WithLogger lets you inject your own logger implementing *slog.Logger. +func WithLogger(logger *slog.Logger) ConfigOpt { return func(config *Config) { config.Logger = logger } @@ -210,7 +209,7 @@ func WithMemberChunkingFilter(memberChunkingFilter MemberChunkingFilter) ConfigO } // BuildClient creates a new Client instance with the given token, Config, gateway handlers, http handlers os, name, github & version. -func BuildClient(token string, config Config, gatewayEventHandlerFunc func(client Client) gateway.EventHandlerFunc, httpServerEventHandlerFunc func(client Client) httpserver.EventHandlerFunc, os string, name string, github string, version string) (Client, error) { +func BuildClient(token string, cfg *Config, gatewayEventHandlerFunc func(client Client) gateway.EventHandlerFunc, httpServerEventHandlerFunc func(client Client) httpserver.EventHandlerFunc, os string, name string, github string, version string) (Client, error) { if token == "" { return nil, discord.ErrNoBotToken } @@ -220,62 +219,62 @@ func BuildClient(token string, config Config, gatewayEventHandlerFunc func(clien } client := &clientImpl{ token: token, - logger: config.Logger, + logger: cfg.Logger, } client.applicationID = *id - if config.RestClient == nil { + if cfg.RestClient == nil { // prepend standard user-agent. this can be overridden as it's appended to the front of the slice - config.RestClientConfigOpts = append([]rest.ConfigOpt{ + cfg.RestClientConfigOpts = append([]rest.ConfigOpt{ rest.WithUserAgent(fmt.Sprintf("DiscordBot (%s, %s)", github, version)), rest.WithLogger(client.logger), func(config *rest.Config) { - config.RateRateLimiterConfigOpts = append([]rest.RateLimiterConfigOpt{rest.WithRateLimiterLogger(client.logger)}, config.RateRateLimiterConfigOpts...) + config.RateLimiterConfigOpts = append([]rest.RateLimiterConfigOpt{rest.WithRateLimiterLogger(cfg.Logger)}, config.RateLimiterConfigOpts...) }, - }, config.RestClientConfigOpts...) + }, cfg.RestClientConfigOpts...) - config.RestClient = rest.NewClient(client.token, config.RestClientConfigOpts...) + cfg.RestClient = rest.NewClient(client.token, cfg.RestClientConfigOpts...) } - if config.Rest == nil { - config.Rest = rest.New(config.RestClient) + if cfg.Rest == nil { + cfg.Rest = rest.New(cfg.RestClient) } - client.restServices = config.Rest + client.restServices = cfg.Rest - if config.VoiceManager == nil { - config.VoiceManager = voice.NewManager(client.UpdateVoiceState, *id, append([]voice.ManagerConfigOpt{voice.WithLogger(client.logger)}, config.VoiceManagerConfigOpts...)...) + if cfg.VoiceManager == nil { + cfg.VoiceManager = voice.NewManager(client.UpdateVoiceState, *id, append([]voice.ManagerConfigOpt{voice.WithLogger(cfg.Logger)}, cfg.VoiceManagerConfigOpts...)...) } - client.voiceManager = config.VoiceManager + client.voiceManager = cfg.VoiceManager - if config.EventManager == nil { - config.EventManager = NewEventManager(client, config.EventManagerConfigOpts...) + if cfg.EventManager == nil { + cfg.EventManager = NewEventManager(client, append([]EventManagerConfigOpt{WithEventManagerLogger(cfg.Logger)}, cfg.EventManagerConfigOpts...)...) } - client.eventManager = config.EventManager + client.eventManager = cfg.EventManager - if config.Gateway == nil && len(config.GatewayConfigOpts) > 0 { + if cfg.Gateway == nil && len(cfg.GatewayConfigOpts) > 0 { var gatewayRs *discord.Gateway gatewayRs, err = client.restServices.GetGateway() if err != nil { return nil, err } - config.GatewayConfigOpts = append([]gateway.ConfigOpt{ + cfg.GatewayConfigOpts = append([]gateway.ConfigOpt{ gateway.WithURL(gatewayRs.URL), - gateway.WithLogger(client.logger), + gateway.WithLogger(cfg.Logger), gateway.WithOS(os), gateway.WithBrowser(name), gateway.WithDevice(name), func(config *gateway.Config) { - config.RateRateLimiterConfigOpts = append([]gateway.RateLimiterConfigOpt{gateway.WithRateLimiterLogger(client.logger)}, config.RateRateLimiterConfigOpts...) + config.RateLimiterConfigOpts = append([]gateway.RateLimiterConfigOpt{gateway.WithRateLimiterLogger(cfg.Logger)}, config.RateLimiterConfigOpts...) }, - }, config.GatewayConfigOpts...) + }, cfg.GatewayConfigOpts...) - config.Gateway = gateway.New(token, gatewayEventHandlerFunc(client), nil, config.GatewayConfigOpts...) + cfg.Gateway = gateway.New(token, gatewayEventHandlerFunc(client), nil, cfg.GatewayConfigOpts...) } - client.gateway = config.Gateway + client.gateway = cfg.Gateway - if config.ShardManager == nil && len(config.ShardManagerConfigOpts) > 0 { + if cfg.ShardManager == nil && len(cfg.ShardManagerConfigOpts) > 0 { var gatewayBotRs *discord.GatewayBot gatewayBotRs, err = client.restServices.GetGatewayBot() if err != nil { @@ -287,47 +286,47 @@ func BuildClient(token string, config Config, gatewayEventHandlerFunc func(clien shardIDs[i] = i } - config.ShardManagerConfigOpts = append([]sharding.ConfigOpt{ + cfg.ShardManagerConfigOpts = append([]sharding.ConfigOpt{ sharding.WithShardCount(gatewayBotRs.Shards), sharding.WithShardIDs(shardIDs...), sharding.WithGatewayConfigOpts( gateway.WithURL(gatewayBotRs.URL), - gateway.WithLogger(client.logger), + gateway.WithLogger(cfg.Logger), gateway.WithOS(os), gateway.WithBrowser(name), gateway.WithDevice(name), func(config *gateway.Config) { - config.RateRateLimiterConfigOpts = append([]gateway.RateLimiterConfigOpt{gateway.WithRateLimiterLogger(client.logger)}, config.RateRateLimiterConfigOpts...) + config.RateLimiterConfigOpts = append([]gateway.RateLimiterConfigOpt{gateway.WithRateLimiterLogger(cfg.Logger)}, config.RateLimiterConfigOpts...) }, ), - sharding.WithLogger(client.logger), + sharding.WithLogger(cfg.Logger), func(config *sharding.Config) { - config.RateRateLimiterConfigOpts = append([]sharding.RateLimiterConfigOpt{sharding.WithRateLimiterLogger(client.logger), sharding.WithMaxConcurrency(gatewayBotRs.SessionStartLimit.MaxConcurrency)}, config.RateRateLimiterConfigOpts...) + config.RateLimiterConfigOpts = append([]sharding.RateLimiterConfigOpt{sharding.WithRateLimiterLogger(cfg.Logger), sharding.WithMaxConcurrency(gatewayBotRs.SessionStartLimit.MaxConcurrency)}, config.RateLimiterConfigOpts...) }, - }, config.ShardManagerConfigOpts...) + }, cfg.ShardManagerConfigOpts...) - config.ShardManager = sharding.New(token, gatewayEventHandlerFunc(client), config.ShardManagerConfigOpts...) + cfg.ShardManager = sharding.New(token, gatewayEventHandlerFunc(client), cfg.ShardManagerConfigOpts...) } - client.shardManager = config.ShardManager + client.shardManager = cfg.ShardManager - if config.HTTPServer == nil && config.PublicKey != "" { - config.HTTPServerConfigOpts = append([]httpserver.ConfigOpt{ - httpserver.WithLogger(client.logger), - }, config.HTTPServerConfigOpts...) + if cfg.HTTPServer == nil && cfg.PublicKey != "" { + cfg.HTTPServerConfigOpts = append([]httpserver.ConfigOpt{ + httpserver.WithLogger(cfg.Logger), + }, cfg.HTTPServerConfigOpts...) - config.HTTPServer = httpserver.New(config.PublicKey, httpServerEventHandlerFunc(client), config.HTTPServerConfigOpts...) + cfg.HTTPServer = httpserver.New(cfg.PublicKey, httpServerEventHandlerFunc(client), cfg.HTTPServerConfigOpts...) } - client.httpServer = config.HTTPServer + client.httpServer = cfg.HTTPServer - if config.MemberChunkingManager == nil { - config.MemberChunkingManager = NewMemberChunkingManager(client, config.Logger, config.MemberChunkingFilter) + if cfg.MemberChunkingManager == nil { + cfg.MemberChunkingManager = NewMemberChunkingManager(client, cfg.Logger, cfg.MemberChunkingFilter) } - client.memberChunkingManager = config.MemberChunkingManager + client.memberChunkingManager = cfg.MemberChunkingManager - if config.Caches == nil { - config.Caches = cache.New(config.CacheConfigOpts...) + if cfg.Caches == nil { + cfg.Caches = cache.New(cfg.CacheConfigOpts...) } - client.caches = config.Caches + client.caches = cfg.Caches return client, nil } diff --git a/bot/event_manager.go b/bot/event_manager.go index 84565fed3..7083ae864 100644 --- a/bot/event_manager.go +++ b/bot/event_manager.go @@ -1,6 +1,7 @@ package bot import ( + "log/slog" "runtime/debug" "sync" @@ -12,12 +13,17 @@ var _ EventManager = (*eventManagerImpl)(nil) // NewEventManager returns a new EventManager with the EventManagerConfigOpt(s) applied. func NewEventManager(client Client, opts ...EventManagerConfigOpt) EventManager { - config := DefaultEventManagerConfig() - config.Apply(opts) + cfg := DefaultEventManagerConfig() + cfg.Apply(opts) + cfg.Logger = cfg.Logger.With(slog.String("name", "bot_event_manager")) return &eventManagerImpl{ - client: client, - config: *config, + client: client, + logger: cfg.Logger, + eventListeners: cfg.EventListeners, + asyncEventsEnabled: cfg.AsyncEventsEnabled, + gatewayHandlers: cfg.GatewayHandlers, + httpServerHandler: cfg.HTTPServerHandler, } } @@ -112,68 +118,72 @@ type HTTPServerEventHandler interface { } type eventManagerImpl struct { - client Client - eventListenerMu sync.Mutex - config EventManagerConfig - mu sync.Mutex + + client Client + logger *slog.Logger + eventListenerMu sync.Mutex + eventListeners []EventListener + asyncEventsEnabled bool + gatewayHandlers map[gateway.EventType]GatewayEventHandler + httpServerHandler HTTPServerEventHandler } func (e *eventManagerImpl) HandleGatewayEvent(gatewayEventType gateway.EventType, sequenceNumber int, shardID int, event gateway.EventData) { e.mu.Lock() defer e.mu.Unlock() - if handler, ok := e.config.GatewayHandlers[gatewayEventType]; ok { + if handler, ok := e.gatewayHandlers[gatewayEventType]; ok { handler.HandleGatewayEvent(e.client, sequenceNumber, shardID, event) } else { - e.config.Logger.Warnf("no handler for gateway event '%s' found", gatewayEventType) + e.logger.Warn("no handler for gateway event found", slog.Any("event_type", gatewayEventType)) } } func (e *eventManagerImpl) HandleHTTPEvent(respondFunc httpserver.RespondFunc, event httpserver.EventInteractionCreate) { e.mu.Lock() defer e.mu.Unlock() - e.config.HTTPServerHandler.HandleHTTPEvent(e.client, respondFunc, event) + e.httpServerHandler.HandleHTTPEvent(e.client, respondFunc, event) } func (e *eventManagerImpl) DispatchEvent(event Event) { defer func() { if r := recover(); r != nil { - e.config.Logger.Errorf("recovered from panic in event listener: %+v\nstack: %s", r, string(debug.Stack())) + e.logger.Error("recovered from panic in event listener", slog.Any("arg", r), slog.String("stack", string(debug.Stack()))) return } }() e.eventListenerMu.Lock() defer e.eventListenerMu.Unlock() - for i := range e.config.EventListeners { - if e.config.AsyncEventsEnabled { + for i := range e.eventListeners { + if e.asyncEventsEnabled { go func(i int) { defer func() { if r := recover(); r != nil { - e.config.Logger.Errorf("recovered from panic in event listener: %+v\nstack: %s", r, string(debug.Stack())) + e.logger.Error("recovered from panic in event listener", slog.Any("arg", r), slog.String("stack", string(debug.Stack()))) return } }() - e.config.EventListeners[i].OnEvent(event) + e.eventListeners[i].OnEvent(event) }(i) continue } - e.config.EventListeners[i].OnEvent(event) + e.eventListeners[i].OnEvent(event) } } func (e *eventManagerImpl) AddEventListeners(listeners ...EventListener) { e.eventListenerMu.Lock() defer e.eventListenerMu.Unlock() - e.config.EventListeners = append(e.config.EventListeners, listeners...) + e.eventListeners = append(e.eventListeners, listeners...) } func (e *eventManagerImpl) RemoveEventListeners(listeners ...EventListener) { e.eventListenerMu.Lock() defer e.eventListenerMu.Unlock() for _, listener := range listeners { - for i, l := range e.config.EventListeners { + for i, l := range e.eventListeners { if l == listener { - e.config.EventListeners = append(e.config.EventListeners[:i], e.config.EventListeners[i+1:]...) + e.eventListeners = append(e.eventListeners[:i], e.eventListeners[i+1:]...) break } } diff --git a/bot/event_manager_config.go b/bot/event_manager_config.go index e62e72d41..2bf40d427 100644 --- a/bot/event_manager_config.go +++ b/bot/event_manager_config.go @@ -1,7 +1,7 @@ package bot import ( - "github.com/disgoorg/log" + "log/slog" "github.com/disgoorg/disgo/gateway" ) @@ -9,13 +9,13 @@ import ( // DefaultEventManagerConfig returns a new EventManagerConfig with all default values. func DefaultEventManagerConfig() *EventManagerConfig { return &EventManagerConfig{ - Logger: log.Default(), + Logger: slog.Default(), } } // EventManagerConfig can be used to configure the EventManager. type EventManagerConfig struct { - Logger log.Logger + Logger *slog.Logger EventListeners []EventListener AsyncEventsEnabled bool @@ -34,7 +34,7 @@ func (c *EventManagerConfig) Apply(opts []EventManagerConfigOpt) { } // WithEventManagerLogger overrides the default logger in the EventManagerConfig. -func WithEventManagerLogger(logger log.Logger) EventManagerConfigOpt { +func WithEventManagerLogger(logger *slog.Logger) EventManagerConfigOpt { return func(config *EventManagerConfig) { config.Logger = logger } diff --git a/bot/member_chunking_filter.go b/bot/member_chunking_filter.go index 2216efb61..6a2348b73 100644 --- a/bot/member_chunking_filter.go +++ b/bot/member_chunking_filter.go @@ -1,8 +1,9 @@ package bot import ( + "slices" + "github.com/disgoorg/snowflake/v2" - "golang.org/x/exp/slices" ) // MemberChunkingFilterAll is a MemberChunkingFilter which includes all guilds. diff --git a/bot/member_chunking_manager.go b/bot/member_chunking_manager.go index 283110000..7be1646da 100644 --- a/bot/member_chunking_manager.go +++ b/bot/member_chunking_manager.go @@ -2,9 +2,10 @@ package bot import ( "context" + "errors" + "log/slog" "sync" - "github.com/disgoorg/log" "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" @@ -14,14 +15,18 @@ import ( var _ MemberChunkingManager = (*memberChunkingManagerImpl)(nil) +var ErrNoUserIDs = errors.New("no user ids to request") + // NewMemberChunkingManager returns a new MemberChunkingManager with the given MemberChunkingFilter. -func NewMemberChunkingManager(client Client, logger log.Logger, memberChunkingFilter MemberChunkingFilter) MemberChunkingManager { +func NewMemberChunkingManager(client Client, logger *slog.Logger, memberChunkingFilter MemberChunkingFilter) MemberChunkingManager { if memberChunkingFilter == nil { memberChunkingFilter = MemberChunkingFilterNone } if logger == nil { - logger = log.Default() + logger = slog.Default() } + logger = logger.With(slog.String("name", "bot_member_chunking_manager")) + return &memberChunkingManagerImpl{ client: client, logger: logger, @@ -41,6 +46,9 @@ type MemberChunkingManager interface { // RequestMembers requests members from the given guildID and userIDs. // Notice: This action requires the gateway.IntentGuildMembers. RequestMembers(guildID snowflake.ID, userIDs ...snowflake.ID) ([]discord.Member, error) + // RequestAllMembers requests all members from the given guildID. + // Notice: This action requires the gateway.IntentGuildMembers. + RequestAllMembers(guildID snowflake.ID) ([]discord.Member, error) // RequestMembersWithQuery requests members from the given guildID and query. // query : string the username starts with // Notice: This action requires the gateway.IntentGuildMembers. @@ -52,6 +60,9 @@ type MemberChunkingManager interface { // RequestMembersCtx requests members from the given guildID and userIDs. // Notice: This action requires the gateway.IntentGuildMembers. RequestMembersCtx(ctx context.Context, guildID snowflake.ID, userIDs ...snowflake.ID) ([]discord.Member, error) + // RequestAllMembersCtx requests all members from the given guildID. + // Notice: This action requires the gateway.IntentGuildMembers. + RequestAllMembersCtx(ctx context.Context, guildID snowflake.ID) ([]discord.Member, error) // RequestMembersWithQueryCtx requests members from the given guildID and query. // Notice: This action requires the gateway.IntentGuildMembers. RequestMembersWithQueryCtx(ctx context.Context, guildID snowflake.ID, query string, limit int) ([]discord.Member, error) @@ -64,6 +75,11 @@ type MemberChunkingManager interface { // Returns a function which can be used to cancel the request and close the channel. // Notice: This action requires the gateway.IntentGuildMembers. RequestMembersChan(guildID snowflake.ID, userIDs ...snowflake.ID) (<-chan discord.Member, func(), error) + // RequestAllMembersChan requests all members from the given guildID. + // Returns a channel which will receive the members. + // Returns a function which can be used to cancel the request and close the channel. + // Notice: This action requires the gateway.IntentGuildMembers. + RequestAllMembersChan(guildID snowflake.ID) (<-chan discord.Member, func(), error) // RequestMembersWithQueryChan requests members from the given guildID and query. // Returns a channel which will receive the members. // Returns a function which can be used to cancel the request and close the channel. @@ -88,7 +104,7 @@ type chunkingRequest struct { type memberChunkingManagerImpl struct { client Client - logger log.Logger + logger *slog.Logger memberChunkingFilter MemberChunkingFilter chunkingRequestsMu sync.RWMutex @@ -104,7 +120,7 @@ func (m *memberChunkingManagerImpl) HandleChunk(payload gateway.EventGuildMember request, ok := m.chunkingRequests[payload.Nonce] m.chunkingRequestsMu.RUnlock() if !ok { - m.logger.Debug("received unknown member chunk event: ", payload) + m.logger.Debug("received unknown member chunk event", slog.Any("payload", payload)) return } @@ -214,6 +230,9 @@ func (m *memberChunkingManagerImpl) RequestMembersWithFilter(guildID snowflake.I } func (m *memberChunkingManagerImpl) RequestMembersCtx(ctx context.Context, guildID snowflake.ID, userIDs ...snowflake.ID) ([]discord.Member, error) { + if len(userIDs) == 0 { + return nil, ErrNoUserIDs + } return m.requestGuildMembers(ctx, guildID, nil, nil, userIDs, nil) } @@ -234,6 +253,9 @@ func (m *memberChunkingManagerImpl) RequestMembersWithFilterCtx(ctx context.Cont } func (m *memberChunkingManagerImpl) RequestMembersChan(guildID snowflake.ID, userIDs ...snowflake.ID) (<-chan discord.Member, func(), error) { + if len(userIDs) == 0 { + return nil, nil, ErrNoUserIDs + } return m.requestGuildMembersChan(context.Background(), guildID, nil, nil, userIDs, nil) } diff --git a/cache/cache_config.go b/cache/cache_config.go index dc0395452..6ed76a166 100644 --- a/cache/cache_config.go +++ b/cache/cache_config.go @@ -9,18 +9,19 @@ import ( // DefaultConfig returns a Config with sensible defaults. func DefaultConfig() *Config { return &Config{ - GuildCachePolicy: PolicyAll[discord.Guild], - ChannelCachePolicy: PolicyAll[discord.GuildChannel], - StageInstanceCachePolicy: PolicyAll[discord.StageInstance], - GuildScheduledEventCachePolicy: PolicyAll[discord.GuildScheduledEvent], - RoleCachePolicy: PolicyAll[discord.Role], - MemberCachePolicy: PolicyAll[discord.Member], - ThreadMemberCachePolicy: PolicyAll[discord.ThreadMember], - PresenceCachePolicy: PolicyAll[discord.Presence], - VoiceStateCachePolicy: PolicyAll[discord.VoiceState], - MessageCachePolicy: PolicyAll[discord.Message], - EmojiCachePolicy: PolicyAll[discord.Emoji], - StickerCachePolicy: PolicyAll[discord.Sticker], + GuildCachePolicy: PolicyAll[discord.Guild], + ChannelCachePolicy: PolicyAll[discord.GuildChannel], + StageInstanceCachePolicy: PolicyAll[discord.StageInstance], + GuildScheduledEventCachePolicy: PolicyAll[discord.GuildScheduledEvent], + GuildSoundboardSoundCachePolicy: PolicyAll[discord.SoundboardSound], + RoleCachePolicy: PolicyAll[discord.Role], + MemberCachePolicy: PolicyAll[discord.Member], + ThreadMemberCachePolicy: PolicyAll[discord.ThreadMember], + PresenceCachePolicy: PolicyAll[discord.Presence], + VoiceStateCachePolicy: PolicyAll[discord.VoiceState], + MessageCachePolicy: PolicyAll[discord.Message], + EmojiCachePolicy: PolicyAll[discord.Emoji], + StickerCachePolicy: PolicyAll[discord.Sticker], } } @@ -42,6 +43,9 @@ type Config struct { GuildScheduledEventCache GuildScheduledEventCache GuildScheduledEventCachePolicy Policy[discord.GuildScheduledEvent] + GuildSoundboardSoundCache GuildSoundboardSoundCache + GuildSoundboardSoundCachePolicy Policy[discord.SoundboardSound] + RoleCache RoleCache RoleCachePolicy Policy[discord.Role] @@ -90,6 +94,9 @@ func (c *Config) Apply(opts []ConfigOpt) { if c.GuildScheduledEventCache == nil { c.GuildScheduledEventCache = NewGuildScheduledEventCache(NewGroupedCache[discord.GuildScheduledEvent](c.CacheFlags, FlagGuildScheduledEvents, c.GuildScheduledEventCachePolicy)) } + if c.GuildSoundboardSoundCache == nil { + c.GuildSoundboardSoundCache = NewGuildSoundboardSoundCache(NewGroupedCache[discord.SoundboardSound](c.CacheFlags, FlagGuildSoundboardSounds, c.GuildSoundboardSoundCachePolicy)) + } if c.RoleCache == nil { c.RoleCache = NewRoleCache(NewGroupedCache[discord.Role](c.CacheFlags, FlagRoles, c.RoleCachePolicy)) } @@ -179,6 +186,13 @@ func WithGuildScheduledEventCache(guildScheduledEventCache GuildScheduledEventCa } } +// WithGuildSoundboardSoundCache sets the GuildSoundboardSoundCache of the Config. +func WithGuildSoundboardSoundCache(guildSoundboardSoundCache GuildSoundboardSoundCache) ConfigOpt { + return func(config *Config) { + config.GuildSoundboardSoundCache = guildSoundboardSoundCache + } +} + // WithRoleCachePolicy sets the Policy[discord.Role] of the Config. func WithRoleCachePolicy(policy Policy[discord.Role]) ConfigOpt { return func(config *Config) { diff --git a/cache/cache_flags.go b/cache/cache_flags.go index 2be7d5f5f..dc3b80fb4 100644 --- a/cache/cache_flags.go +++ b/cache/cache_flags.go @@ -19,6 +19,7 @@ const ( FlagStickers FlagVoiceStates FlagStageInstances + FlagGuildSoundboardSounds FlagsNone Flags = 0 FlagsAll = FlagGuilds | @@ -32,7 +33,8 @@ const ( FlagEmojis | FlagStickers | FlagVoiceStates | - FlagStageInstances + FlagStageInstances | + FlagGuildSoundboardSounds ) // Add allows you to add multiple bits together, producing a new bit diff --git a/cache/cache_policy.go b/cache/cache_policy.go index 195024f17..a54e0587f 100644 --- a/cache/cache_policy.go +++ b/cache/cache_policy.go @@ -1,10 +1,10 @@ package cache import ( - "github.com/disgoorg/snowflake/v2" - "golang.org/x/exp/slices" + "slices" "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" ) // PolicyNone returns a policy that will never cache anything. diff --git a/cache/caches.go b/cache/caches.go index 32fe51725..dbc9cdfa9 100644 --- a/cache/caches.go +++ b/cache/caches.go @@ -2,6 +2,7 @@ package cache import ( "sync" + "time" "github.com/disgoorg/snowflake/v2" @@ -40,6 +41,8 @@ func (c *selfUserCacheImpl) SetSelfUser(user discord.OAuth2User) { } type GuildCache interface { + GuildCache() Cache[discord.Guild] + IsGuildUnready(guildID snowflake.ID) bool SetGuildUnready(guildID snowflake.ID, unready bool) UnreadyGuildIDs() []snowflake.ID @@ -69,6 +72,10 @@ type guildCacheImpl struct { unavailableGuilds Set[snowflake.ID] } +func (c *guildCacheImpl) GuildCache() Cache[discord.Guild] { + return c.cache +} + func (c *guildCacheImpl) IsGuildUnready(guildID snowflake.ID) bool { return c.unreadyGuilds.Has(guildID) } @@ -130,6 +137,8 @@ func (c *guildCacheImpl) RemoveGuild(guildID snowflake.ID) (discord.Guild, bool) } type ChannelCache interface { + ChannelCache() Cache[discord.GuildChannel] + Channel(channelID snowflake.ID) (discord.GuildChannel, bool) ChannelsForEach(fn func(channel discord.GuildChannel)) ChannelsLen() int @@ -148,6 +157,10 @@ type channelCacheImpl struct { cache Cache[discord.GuildChannel] } +func (c *channelCacheImpl) ChannelCache() Cache[discord.GuildChannel] { + return c.cache +} + func (c *channelCacheImpl) Channel(channelID snowflake.ID) (discord.GuildChannel, bool) { return c.cache.Get(channelID) } @@ -175,6 +188,8 @@ func (c *channelCacheImpl) RemoveChannelsByGuildID(guildID snowflake.ID) { } type StageInstanceCache interface { + StageInstanceCache() GroupedCache[discord.StageInstance] + StageInstance(guildID snowflake.ID, stageInstanceID snowflake.ID) (discord.StageInstance, bool) StageInstanceForEach(guildID snowflake.ID, fn func(stageInstance discord.StageInstance)) StageInstancesAllLen() int @@ -194,6 +209,10 @@ type stageInstanceCacheImpl struct { cache GroupedCache[discord.StageInstance] } +func (c *stageInstanceCacheImpl) StageInstanceCache() GroupedCache[discord.StageInstance] { + return c.cache +} + func (c *stageInstanceCacheImpl) StageInstance(guildID snowflake.ID, stageInstanceID snowflake.ID) (discord.StageInstance, bool) { return c.cache.Get(guildID, stageInstanceID) } @@ -225,6 +244,7 @@ func (c *stageInstanceCacheImpl) RemoveStageInstancesByGuildID(guildID snowflake } type GuildScheduledEventCache interface { + GuildScheduledEventCache() GroupedCache[discord.GuildScheduledEvent] GuildScheduledEvent(guildID snowflake.ID, guildScheduledEventID snowflake.ID) (discord.GuildScheduledEvent, bool) GuildScheduledEventsForEach(guildID snowflake.ID, fn func(guildScheduledEvent discord.GuildScheduledEvent)) GuildScheduledEventsAllLen() int @@ -244,6 +264,10 @@ type guildScheduledEventCacheImpl struct { cache GroupedCache[discord.GuildScheduledEvent] } +func (c *guildScheduledEventCacheImpl) GuildScheduledEventCache() GroupedCache[discord.GuildScheduledEvent] { + return c.cache +} + func (c *guildScheduledEventCacheImpl) GuildScheduledEvent(guildID snowflake.ID, guildScheduledEventID snowflake.ID) (discord.GuildScheduledEvent, bool) { return c.cache.Get(guildID, guildScheduledEventID) } @@ -272,7 +296,62 @@ func (c *guildScheduledEventCacheImpl) RemoveGuildScheduledEventsByGuildID(guild c.cache.GroupRemove(guildID) } +type GuildSoundboardSoundCache interface { + GuildSoundboardSoundCache() GroupedCache[discord.SoundboardSound] + GuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID) (discord.SoundboardSound, bool) + GuildSoundboardSoundsForEach(guildID snowflake.ID, fn func(sound discord.SoundboardSound)) + GuildSoundboardSoundsAllLen() int + GuildSoundboardSoundsLen(guildID snowflake.ID) int + AddGuildSoundboardSound(sound discord.SoundboardSound) + RemoveGuildSoundboardSound(guildID snowflake.ID, sound snowflake.ID) (discord.SoundboardSound, bool) + RemoveGuildSoundboardSoundsByGuildID(guildID snowflake.ID) +} + +func NewGuildSoundboardSoundCache(cache GroupedCache[discord.SoundboardSound]) GuildSoundboardSoundCache { + return &guildSoundboardSoundCacheImpl{ + cache: cache, + } +} + +type guildSoundboardSoundCacheImpl struct { + cache GroupedCache[discord.SoundboardSound] +} + +func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSoundCache() GroupedCache[discord.SoundboardSound] { + return c.cache +} + +func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID) (discord.SoundboardSound, bool) { + return c.cache.Get(guildID, soundID) +} + +func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSoundsForEach(guildID snowflake.ID, fn func(sound discord.SoundboardSound)) { + c.cache.GroupForEach(guildID, fn) +} + +func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSoundsAllLen() int { + return c.cache.Len() +} + +func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSoundsLen(guildID snowflake.ID) int { + return c.cache.GroupLen(guildID) +} + +func (c *guildSoundboardSoundCacheImpl) AddGuildSoundboardSound(sound discord.SoundboardSound) { + c.cache.Put(*sound.GuildID, sound.SoundID, sound) +} + +func (c *guildSoundboardSoundCacheImpl) RemoveGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID) (discord.SoundboardSound, bool) { + return c.cache.Remove(guildID, soundID) +} + +func (c *guildSoundboardSoundCacheImpl) RemoveGuildSoundboardSoundsByGuildID(guildID snowflake.ID) { + c.cache.GroupRemove(guildID) +} + type RoleCache interface { + RoleCache() GroupedCache[discord.Role] + Role(guildID snowflake.ID, roleID snowflake.ID) (discord.Role, bool) RolesForEach(guildID snowflake.ID, fn func(role discord.Role)) RolesAllLen() int @@ -292,6 +371,10 @@ type roleCacheImpl struct { cache GroupedCache[discord.Role] } +func (c *roleCacheImpl) RoleCache() GroupedCache[discord.Role] { + return c.cache +} + func (c *roleCacheImpl) Role(guildID snowflake.ID, roleID snowflake.ID) (discord.Role, bool) { return c.cache.Get(guildID, roleID) } @@ -321,6 +404,8 @@ func (c *roleCacheImpl) RemoveRolesByGuildID(guildID snowflake.ID) { } type MemberCache interface { + MemberCache() GroupedCache[discord.Member] + Member(guildID snowflake.ID, userID snowflake.ID) (discord.Member, bool) MembersForEach(guildID snowflake.ID, fn func(member discord.Member)) MembersAllLen() int @@ -340,6 +425,10 @@ type memberCacheImpl struct { cache GroupedCache[discord.Member] } +func (c *memberCacheImpl) MemberCache() GroupedCache[discord.Member] { + return c.cache +} + func (c *memberCacheImpl) Member(guildID snowflake.ID, userID snowflake.ID) (discord.Member, bool) { return c.cache.Get(guildID, userID) } @@ -369,6 +458,8 @@ func (c *memberCacheImpl) RemoveMembersByGuildID(guildID snowflake.ID) { } type ThreadMemberCache interface { + ThreadMemberCache() GroupedCache[discord.ThreadMember] + ThreadMember(threadID snowflake.ID, userID snowflake.ID) (discord.ThreadMember, bool) ThreadMemberForEach(threadID snowflake.ID, fn func(threadMember discord.ThreadMember)) ThreadMembersAllLen() int @@ -388,6 +479,10 @@ type threadMemberCacheImpl struct { cache GroupedCache[discord.ThreadMember] } +func (c *threadMemberCacheImpl) ThreadMemberCache() GroupedCache[discord.ThreadMember] { + return c.cache +} + func (c *threadMemberCacheImpl) ThreadMember(threadID snowflake.ID, userID snowflake.ID) (discord.ThreadMember, bool) { return c.cache.Get(threadID, userID) } @@ -419,6 +514,8 @@ func (c *threadMemberCacheImpl) RemoveThreadMembersByThreadID(threadID snowflake } type PresenceCache interface { + PresenceCache() GroupedCache[discord.Presence] + Presence(guildID snowflake.ID, userID snowflake.ID) (discord.Presence, bool) PresenceForEach(guildID snowflake.ID, fn func(presence discord.Presence)) PresencesAllLen() int @@ -438,6 +535,10 @@ type presenceCacheImpl struct { cache GroupedCache[discord.Presence] } +func (c *presenceCacheImpl) PresenceCache() GroupedCache[discord.Presence] { + return c.cache +} + func (c *presenceCacheImpl) Presence(guildID snowflake.ID, userID snowflake.ID) (discord.Presence, bool) { return c.cache.Get(guildID, userID) } @@ -469,6 +570,8 @@ func (c *presenceCacheImpl) RemovePresencesByGuildID(guildID snowflake.ID) { } type VoiceStateCache interface { + VoiceStateCache() GroupedCache[discord.VoiceState] + VoiceState(guildID snowflake.ID, userID snowflake.ID) (discord.VoiceState, bool) VoiceStatesForEach(guildID snowflake.ID, fn func(discord.VoiceState)) VoiceStatesAllLen() int @@ -488,6 +591,10 @@ type voiceStateCacheImpl struct { cache GroupedCache[discord.VoiceState] } +func (c *voiceStateCacheImpl) VoiceStateCache() GroupedCache[discord.VoiceState] { + return c.cache +} + func (c *voiceStateCacheImpl) VoiceState(guildID snowflake.ID, userID snowflake.ID) (discord.VoiceState, bool) { return c.cache.Get(guildID, userID) } @@ -517,6 +624,8 @@ func (c *voiceStateCacheImpl) RemoveVoiceStatesByGuildID(guildID snowflake.ID) { } type MessageCache interface { + MessageCache() GroupedCache[discord.Message] + Message(channelID snowflake.ID, messageID snowflake.ID) (discord.Message, bool) MessagesForEach(channelID snowflake.ID, fn func(message discord.Message)) MessagesAllLen() int @@ -537,6 +646,10 @@ type messageCacheImpl struct { cache GroupedCache[discord.Message] } +func (c *messageCacheImpl) MessageCache() GroupedCache[discord.Message] { + return c.cache +} + func (c *messageCacheImpl) Message(channelID snowflake.ID, messageID snowflake.ID) (discord.Message, bool) { return c.cache.Get(channelID, messageID) } @@ -572,6 +685,8 @@ func (c *messageCacheImpl) RemoveMessagesByGuildID(guildID snowflake.ID) { } type EmojiCache interface { + EmojiCache() GroupedCache[discord.Emoji] + Emoji(guildID snowflake.ID, emojiID snowflake.ID) (discord.Emoji, bool) EmojisForEach(guildID snowflake.ID, fn func(emoji discord.Emoji)) EmojisAllLen() int @@ -591,6 +706,10 @@ type emojiCacheImpl struct { cache GroupedCache[discord.Emoji] } +func (c *emojiCacheImpl) EmojiCache() GroupedCache[discord.Emoji] { + return c.cache +} + func (c *emojiCacheImpl) Emoji(guildID snowflake.ID, emojiID snowflake.ID) (discord.Emoji, bool) { return c.cache.Get(guildID, emojiID) } @@ -620,6 +739,8 @@ func (c *emojiCacheImpl) RemoveEmojisByGuildID(guildID snowflake.ID) { } type StickerCache interface { + StickerCache() GroupedCache[discord.Sticker] + Sticker(guildID snowflake.ID, stickerID snowflake.ID) (discord.Sticker, bool) StickersForEach(guildID snowflake.ID, fn func(sticker discord.Sticker)) StickersAllLen() int @@ -639,6 +760,10 @@ type stickerCacheImpl struct { cache GroupedCache[discord.Sticker] } +func (c *stickerCacheImpl) StickerCache() GroupedCache[discord.Sticker] { + return c.cache +} + func (c *stickerCacheImpl) Sticker(guildID snowflake.ID, stickerID snowflake.ID) (discord.Sticker, bool) { return c.cache.Get(guildID, stickerID) } @@ -677,6 +802,7 @@ type Caches interface { ChannelCache StageInstanceCache GuildScheduledEventCache + GuildSoundboardSoundCache RoleCache MemberCache ThreadMemberCache @@ -758,39 +884,59 @@ func New(opts ...ConfigOpt) Caches { config.Apply(opts) return &cachesImpl{ - config: *config, - SelfUserCache: config.SelfUserCache, - GuildCache: config.GuildCache, - ChannelCache: config.ChannelCache, - StageInstanceCache: config.StageInstanceCache, - GuildScheduledEventCache: config.GuildScheduledEventCache, - RoleCache: config.RoleCache, - MemberCache: config.MemberCache, - ThreadMemberCache: config.ThreadMemberCache, - PresenceCache: config.PresenceCache, - VoiceStateCache: config.VoiceStateCache, - MessageCache: config.MessageCache, - EmojiCache: config.EmojiCache, - StickerCache: config.StickerCache, + config: *config, + selfUserCache: config.SelfUserCache, + guildCache: config.GuildCache, + channelCache: config.ChannelCache, + stageInstanceCache: config.StageInstanceCache, + guildScheduledEventCache: config.GuildScheduledEventCache, + guildSoundboardSoundCache: config.GuildSoundboardSoundCache, + roleCache: config.RoleCache, + memberCache: config.MemberCache, + threadMemberCache: config.ThreadMemberCache, + presenceCache: config.PresenceCache, + voiceStateCache: config.VoiceStateCache, + messageCache: config.MessageCache, + emojiCache: config.EmojiCache, + stickerCache: config.StickerCache, } } +// these type aliases are needed to allow having the GuildCache, ChannelCache, etc. as methods on the cachesImpl struct +type ( + guildCache = GuildCache + channelCache = ChannelCache + stageInstanceCache = StageInstanceCache + guildScheduledEventCache = GuildScheduledEventCache + guildSoundboardSoundCache = GuildSoundboardSoundCache + roleCache = RoleCache + memberCache = MemberCache + threadMemberCache = ThreadMemberCache + presenceCache = PresenceCache + voiceStateCache = VoiceStateCache + messageCache = MessageCache + emojiCache = EmojiCache + stickerCache = StickerCache + selfUserCache = SelfUserCache +) + type cachesImpl struct { config Config - GuildCache - ChannelCache - StageInstanceCache - GuildScheduledEventCache - RoleCache - MemberCache - ThreadMemberCache - PresenceCache - VoiceStateCache - MessageCache - EmojiCache - StickerCache - SelfUserCache + guildCache + channelCache + stageInstanceCache + guildScheduledEventCache + guildSoundboardSoundCache + roleCache + memberCache + threadMemberCache + presenceCache + voiceStateCache + messageCache + emojiCache + stickerCache + selfUserCache } func (c *cachesImpl) CacheFlags() Flags { @@ -813,7 +959,7 @@ func (c *cachesImpl) MemberPermissions(member discord.Member) discord.Permission return discord.PermissionsAll } } - if member.CommunicationDisabledUntil != nil { + if member.CommunicationDisabledUntil != nil && member.CommunicationDisabledUntil.After(time.Now()) { permissions &= discord.PermissionViewChannel | discord.PermissionReadMessageHistory } return permissions @@ -854,7 +1000,7 @@ func (c *cachesImpl) MemberPermissionsInChannel(channel discord.GuildChannel, me permissions &= ^deny permissions |= allow - if member.CommunicationDisabledUntil != nil { + if member.CommunicationDisabledUntil != nil && member.CommunicationDisabledUntil.After(time.Now()) { permissions &= discord.PermissionViewChannel | discord.PermissionReadMessageHistory } diff --git a/discord/activity.go b/discord/activity.go index 0f4f019bf..8e0a91967 100644 --- a/discord/activity.go +++ b/discord/activity.go @@ -27,7 +27,7 @@ type Activity struct { ID string `json:"id"` Name string `json:"name"` Type ActivityType `json:"type"` - URL *string `json:"url"` + URL *string `json:"url,omitempty"` CreatedAt time.Time `json:"created_at"` Timestamps *ActivityTimestamps `json:"timestamps,omitempty"` SyncID *string `json:"sync_id,omitempty"` @@ -40,7 +40,7 @@ type Activity struct { Secrets *ActivitySecrets `json:"secrets,omitempty"` Instance *bool `json:"instance,omitempty"` Flags ActivityFlags `json:"flags,omitempty"` - Buttons []string `json:"buttons"` + Buttons []string `json:"buttons,omitempty"` } func (a *Activity) UnmarshalJSON(data []byte) error { diff --git a/discord/application.go b/discord/application.go index d34705b77..69c1fdeaa 100644 --- a/discord/application.go +++ b/discord/application.go @@ -2,37 +2,44 @@ package discord import ( "fmt" + "slices" "strings" "time" + "github.com/disgoorg/json" "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/internal/flags" ) type Application struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Icon *string `json:"icon,omitempty"` - Description string `json:"description"` - RPCOrigins []string `json:"rpc_origins"` - BotPublic bool `json:"bot_public"` - BotRequireCodeGrant bool `json:"bot_require_code_grant"` - TermsOfServiceURL *string `json:"terms_of_service_url,omitempty"` - PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"` - CustomInstallURL *string `json:"custom_install_url,omitempty"` - RoleConnectionsVerificationURL *string `json:"role_connections_verification_url"` - InstallParams *InstallParams `json:"install_params"` - Tags []string `json:"tags"` - Owner *User `json:"owner,omitempty"` - Summary string `json:"summary"` - VerifyKey string `json:"verify_key"` - Team *Team `json:"team,omitempty"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - PrimarySkuID *snowflake.ID `json:"primary_sku_id,omitempty"` - Slug *string `json:"slug,omitempty"` - CoverImage *string `json:"cover_image,omitempty"` - Flags ApplicationFlags `json:"flags,omitempty"` + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Icon *string `json:"icon,omitempty"` + Description string `json:"description"` + RPCOrigins []string `json:"rpc_origins"` + BotPublic bool `json:"bot_public"` + BotRequireCodeGrant bool `json:"bot_require_code_grant"` + Bot *User `json:"bot,omitempty"` + TermsOfServiceURL *string `json:"terms_of_service_url,omitempty"` + PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"` + CustomInstallURL *string `json:"custom_install_url,omitempty"` + InteractionsEndpointURL *string `json:"interactions_endpoint_url,omitempty"` + RoleConnectionsVerificationURL *string `json:"role_connections_verification_url"` + InstallParams *InstallParams `json:"install_params"` + Tags []string `json:"tags"` + Owner *User `json:"owner,omitempty"` + VerifyKey string `json:"verify_key"` + Team *Team `json:"team,omitempty"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Guild *Guild `json:"guild,omitempty"` + PrimarySkuID *snowflake.ID `json:"primary_sku_id,omitempty"` + Slug *string `json:"slug,omitempty"` + CoverImage *string `json:"cover_image,omitempty"` + Flags ApplicationFlags `json:"flags,omitempty"` + ApproximateGuildCount *int `json:"approximate_guild_count,omitempty"` + ApproximateUserInstallCount *int `json:"approximate_user_install_count,omitempty"` + IntegrationTypesConfig ApplicationIntegrationTypesConfig `json:"integration_types_config"` } func (a Application) IconURL(opts ...CDNOpt) *string { @@ -55,6 +62,19 @@ func (a Application) CreatedAt() time.Time { return a.ID.Time() } +type ApplicationUpdate struct { + CustomInstallURL *string `json:"custom_install_url,omitempty"` + Description *string `json:"description,omitempty"` + RoleConnectionsVerificationURL *string `json:"role_connections_verification_url,omitempty"` + InstallParams *InstallParams `json:"install_params,omitempty"` + Flags *ApplicationFlags `json:"flags,omitempty"` + Icon *json.Nullable[Icon] `json:"icon,omitempty"` + CoverImage *json.Nullable[Icon] `json:"cover_image,omitempty"` + InteractionsEndpointURL *string `json:"interactions_endpoint_url,omitempty"` + Tags []string `json:"tags,omitempty"` + IntegrationTypesConfig *ApplicationIntegrationTypesConfig `json:"integration_types_config,omitempty"` +} + type PartialApplication struct { ID snowflake.ID `json:"id"` Flags ApplicationFlags `json:"flags"` @@ -136,12 +156,7 @@ func SplitScopes(joinedScopes string) []OAuth2Scope { } func HasScope(scope OAuth2Scope, scopes ...OAuth2Scope) bool { - for _, s := range scopes { - if s == scope { - return true - } - } - return false + return slices.Contains(scopes, scope) } type TokenType string @@ -224,21 +239,58 @@ func (t Team) CreatedAt() time.Time { } type TeamMember struct { - MembershipState MembershipState `json:"membership_state"` - Permissions []TeamPermissions `json:"permissions"` - TeamID snowflake.ID `json:"team_id"` - User User `json:"user"` + MembershipState MembershipState `json:"membership_state"` + TeamID snowflake.ID `json:"team_id"` + User User `json:"user"` + Role TeamRole `json:"role"` } type MembershipState int const ( - MembershipStateInvited = iota + 1 + MembershipStateInvited MembershipState = iota + 1 MembershipStateAccepted ) -type TeamPermissions string +type TeamRole string + +const ( + TeamRoleAdmin TeamRole = "admin" + TeamRoleDeveloper TeamRole = "developer" + TeamRoleReadOnly TeamRole = "read_only" +) + +type ApplicationIntegrationType int + +const ( + ApplicationIntegrationTypeGuildInstall ApplicationIntegrationType = iota + ApplicationIntegrationTypeUserInstall +) + +type ApplicationIntegrationTypesConfig map[ApplicationIntegrationType]ApplicationIntegrationTypeConfiguration + +type ApplicationIntegrationTypeConfiguration struct { + OAuth2InstallParams *InstallParams `json:"oauth2_install_params"` +} + +type ActivityInstance struct { + ApplicationID snowflake.ID `json:"application_id"` + InstanceID string `json:"instance_id"` + LaunchID snowflake.ID `json:"launch_id"` + Location ActivityLocation `json:"location"` + Users []snowflake.ID `json:"users"` +} + +type ActivityLocation struct { + ID string `json:"id"` + Kind ActivityLocationKind `json:"kind"` + ChannelID snowflake.ID `json:"channel_id"` + GuildID *snowflake.ID `json:"guild_id"` +} + +type ActivityLocationKind string const ( - TeamPermissionAdmin = "*" + ActivityLocationKindGC ActivityLocationKind = "gc" + ActivityLocationKindPC ActivityLocationKind = "pc" ) diff --git a/discord/application_command.go b/discord/application_command.go index a0e06b78f..faa9bb15b 100644 --- a/discord/application_command.go +++ b/discord/application_command.go @@ -11,9 +11,10 @@ import ( type ApplicationCommandType int const ( - ApplicationCommandTypeSlash = iota + 1 + ApplicationCommandTypeSlash ApplicationCommandType = iota + 1 ApplicationCommandTypeUser ApplicationCommandTypeMessage + ApplicationCommandTypePrimaryEntryPoint ) type ApplicationCommand interface { @@ -30,6 +31,8 @@ type ApplicationCommand interface { Version() snowflake.ID CreatedAt() time.Time NSFW() bool + IntegrationTypes() []ApplicationIntegrationType + Contexts() []InteractionContextType applicationCommand() } @@ -67,6 +70,11 @@ func (u *UnmarshalApplicationCommand) UnmarshalJSON(data []byte) error { err = json.Unmarshal(data, &v) applicationCommand = v + case ApplicationCommandTypePrimaryEntryPoint: + var v EntryPointCommand + err = json.Unmarshal(data, &v) + applicationCommand = v + default: err = fmt.Errorf("unknown application command with type %d received", cType.Type) } @@ -95,6 +103,8 @@ type SlashCommand struct { defaultMemberPermissions Permissions dmPermission bool nsfw bool + integrationTypes []ApplicationIntegrationType + contexts []InteractionContextType version snowflake.ID } @@ -117,6 +127,8 @@ func (c *SlashCommand) UnmarshalJSON(data []byte) error { c.defaultMemberPermissions = v.DefaultMemberPermissions c.dmPermission = v.DMPermission c.nsfw = v.NSFW + c.integrationTypes = v.IntegrationTypes + c.contexts = v.Contexts c.version = v.Version return nil } @@ -137,6 +149,8 @@ func (c SlashCommand) MarshalJSON() ([]byte, error) { DefaultMemberPermissions: c.defaultMemberPermissions, DMPermission: c.dmPermission, NSFW: c.nsfw, + IntegrationTypes: c.integrationTypes, + Contexts: c.contexts, Version: c.version, }) } @@ -172,6 +186,7 @@ func (c SlashCommand) NameLocalized() string { func (c SlashCommand) DefaultMemberPermissions() Permissions { return c.defaultMemberPermissions } + func (c SlashCommand) DMPermission() bool { return c.dmPermission } @@ -180,6 +195,14 @@ func (c SlashCommand) NSFW() bool { return c.nsfw } +func (c SlashCommand) IntegrationTypes() []ApplicationIntegrationType { + return c.integrationTypes +} + +func (c SlashCommand) Contexts() []InteractionContextType { + return c.contexts +} + func (c SlashCommand) Version() snowflake.ID { return c.version } @@ -206,6 +229,8 @@ type UserCommand struct { defaultMemberPermissions Permissions dmPermission bool nsfw bool + integrationTypes []ApplicationIntegrationType + contexts []InteractionContextType version snowflake.ID } @@ -224,6 +249,8 @@ func (c *UserCommand) UnmarshalJSON(data []byte) error { c.defaultMemberPermissions = v.DefaultMemberPermissions c.dmPermission = v.DMPermission c.nsfw = v.NSFW + c.integrationTypes = v.IntegrationTypes + c.contexts = v.Contexts c.version = v.Version return nil } @@ -240,6 +267,8 @@ func (c UserCommand) MarshalJSON() ([]byte, error) { DefaultMemberPermissions: c.defaultMemberPermissions, DMPermission: c.dmPermission, NSFW: c.nsfw, + IntegrationTypes: c.integrationTypes, + Contexts: c.contexts, Version: c.version, }) } @@ -275,6 +304,7 @@ func (c UserCommand) NameLocalized() string { func (c UserCommand) DefaultMemberPermissions() Permissions { return c.defaultMemberPermissions } + func (c UserCommand) DMPermission() bool { return c.dmPermission } @@ -283,6 +313,14 @@ func (c UserCommand) NSFW() bool { return c.nsfw } +func (c UserCommand) IntegrationTypes() []ApplicationIntegrationType { + return c.integrationTypes +} + +func (c UserCommand) Contexts() []InteractionContextType { + return c.contexts +} + func (c UserCommand) Version() snowflake.ID { return c.version } @@ -305,6 +343,8 @@ type MessageCommand struct { defaultMemberPermissions Permissions dmPermission bool nsfw bool + integrationTypes []ApplicationIntegrationType + contexts []InteractionContextType version snowflake.ID } @@ -323,6 +363,8 @@ func (c *MessageCommand) UnmarshalJSON(data []byte) error { c.defaultMemberPermissions = v.DefaultMemberPermissions c.dmPermission = v.DMPermission c.nsfw = v.NSFW + c.integrationTypes = v.IntegrationTypes + c.contexts = v.Contexts c.version = v.Version return nil } @@ -339,6 +381,8 @@ func (c MessageCommand) MarshalJSON() ([]byte, error) { DefaultMemberPermissions: c.defaultMemberPermissions, DMPermission: c.dmPermission, NSFW: c.nsfw, + IntegrationTypes: c.integrationTypes, + Contexts: c.contexts, Version: c.version, }) } @@ -374,6 +418,7 @@ func (c MessageCommand) NameLocalized() string { func (c MessageCommand) DefaultMemberPermissions() Permissions { return c.defaultMemberPermissions } + func (c MessageCommand) DMPermission() bool { return c.dmPermission } @@ -382,6 +427,14 @@ func (c MessageCommand) NSFW() bool { return c.nsfw } +func (c MessageCommand) IntegrationTypes() []ApplicationIntegrationType { + return c.integrationTypes +} + +func (c MessageCommand) Contexts() []InteractionContextType { + return c.contexts +} + func (c MessageCommand) Version() snowflake.ID { return c.version } @@ -391,3 +444,127 @@ func (c MessageCommand) CreatedAt() time.Time { } func (MessageCommand) applicationCommand() {} + +var _ ApplicationCommand = (*EntryPointCommand)(nil) + +type EntryPointCommand struct { + id snowflake.ID + applicationID snowflake.ID + guildID *snowflake.ID + name string + nameLocalizations map[Locale]string + nameLocalized string + defaultMemberPermissions Permissions + dmPermission bool + nsfw bool + integrationTypes []ApplicationIntegrationType + contexts []InteractionContextType + version snowflake.ID + Handler EntryPointCommandHandlerType +} + +func (c *EntryPointCommand) UnmarshalJSON(data []byte) error { + var v rawEntryPointCommand + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + c.id = v.ID + c.applicationID = v.ApplicationID + c.guildID = v.GuildID + c.name = v.Name + c.nameLocalizations = v.NameLocalizations + c.nameLocalized = v.NameLocalized + c.defaultMemberPermissions = v.DefaultMemberPermissions + c.dmPermission = v.DMPermission + c.nsfw = v.NSFW + c.integrationTypes = v.IntegrationTypes + c.contexts = v.Contexts + c.version = v.Version + c.Handler = v.Handler + return nil +} + +func (c EntryPointCommand) MarshalJSON() ([]byte, error) { + return json.Marshal(rawEntryPointCommand{ + ID: c.id, + Type: c.Type(), + ApplicationID: c.applicationID, + GuildID: c.guildID, + Name: c.name, + NameLocalizations: c.nameLocalizations, + NameLocalized: c.nameLocalized, + DefaultMemberPermissions: c.defaultMemberPermissions, + DMPermission: c.dmPermission, + NSFW: c.nsfw, + IntegrationTypes: c.integrationTypes, + Contexts: c.contexts, + Version: c.version, + Handler: c.Handler, + }) +} + +func (c EntryPointCommand) ID() snowflake.ID { + return c.id +} + +func (EntryPointCommand) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (c EntryPointCommand) ApplicationID() snowflake.ID { + return c.applicationID +} + +func (c EntryPointCommand) GuildID() *snowflake.ID { + return c.guildID +} + +func (c EntryPointCommand) Name() string { + return c.name +} + +func (c EntryPointCommand) NameLocalizations() map[Locale]string { + return c.nameLocalizations +} + +func (c EntryPointCommand) NameLocalized() string { + return c.nameLocalized +} + +func (c EntryPointCommand) DefaultMemberPermissions() Permissions { + return c.defaultMemberPermissions +} + +func (c EntryPointCommand) DMPermission() bool { + return c.dmPermission +} + +func (c EntryPointCommand) NSFW() bool { + return c.nsfw +} + +func (c EntryPointCommand) IntegrationTypes() []ApplicationIntegrationType { + return c.integrationTypes +} + +func (c EntryPointCommand) Contexts() []InteractionContextType { + return c.contexts +} + +func (c EntryPointCommand) Version() snowflake.ID { + return c.version +} + +func (c EntryPointCommand) CreatedAt() time.Time { + return c.id.Time() +} + +func (EntryPointCommand) applicationCommand() {} + +type EntryPointCommandHandlerType int + +const ( + EntryPointCommandHandlerTypeAppHandler EntryPointCommandHandlerType = iota + 1 + EntryPointCommandHandlerTypeDiscordLaunchActivity +) diff --git a/discord/application_command_create.go b/discord/application_command_create.go index bf80a0fbd..cbe04f18e 100644 --- a/discord/application_command_create.go +++ b/discord/application_command_create.go @@ -16,8 +16,11 @@ type SlashCommandCreate struct { DescriptionLocalizations map[Locale]string `json:"description_localizations,omitempty"` Options []ApplicationCommandOption `json:"options,omitempty"` DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` // different behavior for 0 and null, optional - DMPermission *bool `json:"dm_permission,omitempty"` - NSFW *bool `json:"nsfw,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts []InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` } func (c SlashCommandCreate) MarshalJSON() ([]byte, error) { @@ -45,8 +48,11 @@ type UserCommandCreate struct { Name string `json:"name"` NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` - DMPermission *bool `json:"dm_permission,omitempty"` - NSFW *bool `json:"nsfw,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts []InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` } func (c UserCommandCreate) MarshalJSON() ([]byte, error) { @@ -74,8 +80,11 @@ type MessageCommandCreate struct { Name string `json:"name"` NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` - DMPermission *bool `json:"dm_permission,omitempty"` - NSFW *bool `json:"nsfw,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts []InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` } func (c MessageCommandCreate) MarshalJSON() ([]byte, error) { @@ -98,3 +107,36 @@ func (c MessageCommandCreate) CommandName() string { } func (MessageCommandCreate) applicationCommandCreate() {} + +type EntryPointCommandCreate struct { + Name string `json:"name"` + NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` + DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts []InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` + Handler EntryPointCommandHandlerType `json:"handler,omitempty"` +} + +func (c EntryPointCommandCreate) MarshalJSON() ([]byte, error) { + type entryPointCommandCreate EntryPointCommandCreate + return json.Marshal(struct { + Type ApplicationCommandType `json:"type"` + entryPointCommandCreate + }{ + Type: c.Type(), + entryPointCommandCreate: entryPointCommandCreate(c), + }) +} + +func (EntryPointCommandCreate) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (c EntryPointCommandCreate) CommandName() string { + return c.Name +} + +func (EntryPointCommandCreate) applicationCommandCreate() {} diff --git a/discord/application_command_option.go b/discord/application_command_option.go index b500f0c59..0647b8913 100644 --- a/discord/application_command_option.go +++ b/discord/application_command_option.go @@ -379,7 +379,7 @@ func (o ApplicationCommandOptionChannel) OptionName() string { } func (o ApplicationCommandOptionChannel) OptionDescription() string { - return o.Name + return o.Description } func (ApplicationCommandOptionChannel) applicationCommandOption() {} @@ -413,7 +413,7 @@ func (o ApplicationCommandOptionRole) OptionName() string { } func (o ApplicationCommandOptionRole) OptionDescription() string { - return o.Name + return o.Description } func (ApplicationCommandOptionRole) applicationCommandOption() {} @@ -447,7 +447,7 @@ func (o ApplicationCommandOptionMentionable) OptionName() string { } func (o ApplicationCommandOptionMentionable) OptionDescription() string { - return o.Name + return o.Description } func (ApplicationCommandOptionMentionable) applicationCommandOption() {} @@ -485,7 +485,7 @@ func (o ApplicationCommandOptionFloat) OptionName() string { } func (o ApplicationCommandOptionFloat) OptionDescription() string { - return o.Name + return o.Description } func (ApplicationCommandOptionFloat) applicationCommandOption() {} @@ -494,6 +494,8 @@ func (ApplicationCommandOptionFloat) Type() ApplicationCommandOptionType { } type ApplicationCommandOptionChoice interface { + ChoiceName() string + applicationCommandOptionChoice() } @@ -505,6 +507,10 @@ type ApplicationCommandOptionChoiceInt struct { Value int `json:"value"` } +func (c ApplicationCommandOptionChoiceInt) ChoiceName() string { + return c.Name +} + func (ApplicationCommandOptionChoiceInt) applicationCommandOptionChoice() {} var _ ApplicationCommandOptionChoice = (*ApplicationCommandOptionChoiceString)(nil) @@ -515,6 +521,10 @@ type ApplicationCommandOptionChoiceString struct { Value string `json:"value"` } +func (c ApplicationCommandOptionChoiceString) ChoiceName() string { + return c.Name +} + func (ApplicationCommandOptionChoiceString) applicationCommandOptionChoice() {} var _ ApplicationCommandOptionChoice = (*ApplicationCommandOptionChoiceInt)(nil) @@ -525,6 +535,10 @@ type ApplicationCommandOptionChoiceFloat struct { Value float64 `json:"value"` } +func (c ApplicationCommandOptionChoiceFloat) ChoiceName() string { + return c.Name +} + func (ApplicationCommandOptionChoiceFloat) applicationCommandOptionChoice() {} type ApplicationCommandOptionAttachment struct { @@ -551,7 +565,7 @@ func (o ApplicationCommandOptionAttachment) OptionName() string { } func (o ApplicationCommandOptionAttachment) OptionDescription() string { - return o.Name + return o.Description } func (ApplicationCommandOptionAttachment) applicationCommandOption() {} diff --git a/discord/application_command_permission.go b/discord/application_command_permission.go index 58c023921..75277a3ec 100644 --- a/discord/application_command_permission.go +++ b/discord/application_command_permission.go @@ -12,7 +12,7 @@ type ApplicationCommandPermissionType int // types of ApplicationCommandPermissionType const ( - ApplicationCommandPermissionTypeRole = iota + 1 + ApplicationCommandPermissionTypeRole ApplicationCommandPermissionType = iota + 1 ApplicationCommandPermissionTypeUser ApplicationCommandPermissionTypeChannel ) diff --git a/discord/application_command_raw.go b/discord/application_command_raw.go index a5fbe038c..aee24e8b8 100644 --- a/discord/application_command_raw.go +++ b/discord/application_command_raw.go @@ -6,21 +6,23 @@ import ( ) type rawSlashCommand struct { - ID snowflake.ID `json:"id"` - Type ApplicationCommandType `json:"type"` - ApplicationID snowflake.ID `json:"application_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Name string `json:"name"` - NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` - NameLocalized string `json:"name_localized,omitempty"` - Description string `json:"description,omitempty"` - DescriptionLocalizations map[Locale]string `json:"description_localizations,omitempty"` - DescriptionLocalized string `json:"description_localized,omitempty"` - Options []ApplicationCommandOption `json:"options,omitempty"` - DefaultMemberPermissions Permissions `json:"default_member_permissions"` - DMPermission bool `json:"dm_permission"` - NSFW bool `json:"nsfw"` - Version snowflake.ID `json:"version"` + ID snowflake.ID `json:"id"` + Type ApplicationCommandType `json:"type"` + ApplicationID snowflake.ID `json:"application_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Name string `json:"name"` + NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` + NameLocalized string `json:"name_localized,omitempty"` + Description string `json:"description,omitempty"` + DescriptionLocalizations map[Locale]string `json:"description_localizations,omitempty"` + DescriptionLocalized string `json:"description_localized,omitempty"` + Options []ApplicationCommandOption `json:"options,omitempty"` + DefaultMemberPermissions Permissions `json:"default_member_permissions"` + DMPermission bool `json:"dm_permission"` + NSFW bool `json:"nsfw"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types"` + Contexts []InteractionContextType `json:"contexts"` + Version snowflake.ID `json:"version"` } func (c *rawSlashCommand) UnmarshalJSON(data []byte) error { @@ -46,15 +48,34 @@ func (c *rawSlashCommand) UnmarshalJSON(data []byte) error { } type rawContextCommand struct { - ID snowflake.ID `json:"id"` - Type ApplicationCommandType `json:"type"` - ApplicationID snowflake.ID `json:"application_id"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - Name string `json:"name"` - NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` - NameLocalized string `json:"name_localized,omitempty"` - DefaultMemberPermissions Permissions `json:"default_member_permissions"` - DMPermission bool `json:"dm_permission"` - NSFW bool `json:"nsfw"` - Version snowflake.ID `json:"version"` + ID snowflake.ID `json:"id"` + Type ApplicationCommandType `json:"type"` + ApplicationID snowflake.ID `json:"application_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Name string `json:"name"` + NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` + NameLocalized string `json:"name_localized,omitempty"` + DefaultMemberPermissions Permissions `json:"default_member_permissions"` + DMPermission bool `json:"dm_permission"` + NSFW bool `json:"nsfw"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types"` + Contexts []InteractionContextType `json:"contexts"` + Version snowflake.ID `json:"version"` +} + +type rawEntryPointCommand struct { + ID snowflake.ID `json:"id"` + Type ApplicationCommandType `json:"type"` + ApplicationID snowflake.ID `json:"application_id"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Name string `json:"name"` + NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` + NameLocalized string `json:"name_localized,omitempty"` + DefaultMemberPermissions Permissions `json:"default_member_permissions"` + DMPermission bool `json:"dm_permission"` + NSFW bool `json:"nsfw"` + IntegrationTypes []ApplicationIntegrationType `json:"integration_types"` + Contexts []InteractionContextType `json:"contexts"` + Version snowflake.ID `json:"version"` + Handler EntryPointCommandHandlerType `json:"handler"` } diff --git a/discord/application_command_update.go b/discord/application_command_update.go index 33764546f..97904cf28 100644 --- a/discord/application_command_update.go +++ b/discord/application_command_update.go @@ -16,8 +16,11 @@ type SlashCommandUpdate struct { DescriptionLocalizations *map[Locale]string `json:"description_localizations,omitempty"` Options *[]ApplicationCommandOption `json:"options,omitempty"` DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` - DMPermission *bool `json:"dm_permission,omitempty"` - NSFW *bool `json:"nsfw,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes *[]ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts *[]InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` } func (c SlashCommandUpdate) MarshalJSON() ([]byte, error) { @@ -45,8 +48,11 @@ type UserCommandUpdate struct { Name *string `json:"name,omitempty"` NameLocalizations *map[Locale]string `json:"name_localizations,omitempty"` DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` - DMPermission *bool `json:"dm_permission,omitempty"` - NSFW *bool `json:"nsfw,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes *[]ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts *[]InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` } func (c UserCommandUpdate) MarshalJSON() ([]byte, error) { @@ -74,8 +80,11 @@ type MessageCommandUpdate struct { Name *string `json:"name,omitempty"` NameLocalizations *map[Locale]string `json:"name_localizations,omitempty"` DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` - DMPermission *bool `json:"dm_permission,omitempty"` - NSFW *bool `json:"nsfw,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes *[]ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts *[]InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` } func (c MessageCommandUpdate) MarshalJSON() ([]byte, error) { @@ -98,3 +107,36 @@ func (c MessageCommandUpdate) CommandName() *string { } func (MessageCommandUpdate) applicationCommandUpdate() {} + +type EntryPointCommandUpdate struct { + Name *string `json:"name,omitempty"` + NameLocalizations *map[Locale]string `json:"name_localizations,omitempty"` + DefaultMemberPermissions *json.Nullable[Permissions] `json:"default_member_permissions,omitempty"` + // Deprecated: Use Contexts instead + DMPermission *bool `json:"dm_permission,omitempty"` + IntegrationTypes *[]ApplicationIntegrationType `json:"integration_types,omitempty"` + Contexts *[]InteractionContextType `json:"contexts,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` + Handler *EntryPointCommandHandlerType `json:"handler,omitempty"` +} + +func (c EntryPointCommandUpdate) MarshalJSON() ([]byte, error) { + type entryPointCommandUpdate EntryPointCommandUpdate + return json.Marshal(struct { + Type ApplicationCommandType `json:"type"` + entryPointCommandUpdate + }{ + Type: c.Type(), + entryPointCommandUpdate: entryPointCommandUpdate(c), + }) +} + +func (EntryPointCommandUpdate) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (c EntryPointCommandUpdate) CommandName() *string { + return c.Name +} + +func (EntryPointCommandUpdate) applicationCommandUpdate() {} diff --git a/discord/attachment.go b/discord/attachment.go index 4a93a5a2e..8d67b40b1 100644 --- a/discord/attachment.go +++ b/discord/attachment.go @@ -10,6 +10,7 @@ import ( type Attachment struct { ID snowflake.ID `json:"id,omitempty"` Filename string `json:"filename,omitempty"` + Title *string `json:"title,omitempty"` Description *string `json:"description,omitempty"` ContentType *string `json:"content_type,omitempty"` Size int `json:"size,omitempty"` diff --git a/discord/audit_log.go b/discord/audit_log.go index 4135c5581..50b5b4dec 100644 --- a/discord/audit_log.go +++ b/discord/audit_log.go @@ -8,12 +8,10 @@ import ( // AuditLogEvent is an 8-bit unsigned integer representing an audit log event. type AuditLogEvent int -// AuditLogEventGuildUpdate ... const ( AuditLogEventGuildUpdate AuditLogEvent = 1 ) -// AuditLogEventChannelCreate const ( AuditLogEventChannelCreate AuditLogEvent = iota + 10 AuditLogEventChannelUpdate @@ -23,7 +21,6 @@ const ( AuditLogEventChannelOverwriteDelete ) -// AuditLogEventMemberKick const ( AuditLogEventMemberKick AuditLogEvent = iota + 20 AuditLogEventMemberPrune @@ -36,35 +33,30 @@ const ( AuditLogEventBotAdd ) -// AuditLogEventRoleCreate const ( AuditLogEventRoleCreate AuditLogEvent = iota + 30 AuditLogEventRoleUpdate AuditLogEventRoleDelete ) -// AuditLogEventInviteCreate const ( AuditLogEventInviteCreate AuditLogEvent = iota + 40 AuditLogEventInviteUpdate AuditLogEventInviteDelete ) -// AuditLogEventWebhookCreate const ( AuditLogEventWebhookCreate AuditLogEvent = iota + 50 AuditLogEventWebhookUpdate AuditLogEventWebhookDelete ) -// AuditLogEventEmojiCreate const ( AuditLogEventEmojiCreate AuditLogEvent = iota + 60 AuditLogEventEmojiUpdate AuditLogEventEmojiDelete ) -// AuditLogEventMessageDelete const ( AuditLogEventMessageDelete AuditLogEvent = iota + 72 AuditLogEventMessageBulkDelete @@ -72,7 +64,6 @@ const ( AuditLogEventMessageUnpin ) -// AuditLogEventIntegrationCreate const ( AuditLogEventIntegrationCreate AuditLogEvent = iota + 80 AuditLogEventIntegrationUpdate @@ -82,32 +73,34 @@ const ( AuditLogEventStageInstanceDelete ) -// AuditLogEventStickerCreate const ( AuditLogEventStickerCreate AuditLogEvent = iota + 90 AuditLogEventStickerUpdate AuditLogEventStickerDelete ) -// AuditLogGuildScheduledEventCreate const ( AuditLogGuildScheduledEventCreate AuditLogEvent = iota + 100 AuditLogGuildScheduledEventUpdate AuditLogGuildScheduledEventDelete ) -// AuditLogThreadCreate const ( AuditLogThreadCreate AuditLogEvent = iota + 110 AuditLogThreadUpdate AuditLogThreadDelete ) -// AuditLogApplicationCommandPermissionUpdate ... const ( AuditLogApplicationCommandPermissionUpdate AuditLogEvent = 121 ) +const ( + AuditLogSoundboardSoundCreate AuditLogEvent = iota + 130 + AuditLogSoundboardSoundUpdate + AuditLogSoundboardSoundDelete +) + const ( AuditLogAutoModerationRuleCreate AuditLogEvent = iota + 140 AuditLogAutoModerationRuleUpdate @@ -122,6 +115,118 @@ const ( AuditLogCreatorMonetizationTermsAccepted ) +const ( + AuditLogOnboardingPromptCreate AuditLogEvent = iota + 163 + AuditLogOnboardingPromptUpdate + AuditLogOnboardingPromptDelete + AuditLogOnboardingCreate + AuditLogOnboardingUpdate +) + +const ( + AuditLogHomeSettingsCreate AuditLogEvent = iota + 190 + AuditLogHomeSettingsUpdate +) + +// AuditLogChangeKey is a string representing a key in the audit log change object. +type AuditLogChangeKey string + +const ( + AuditLogChangeKeyAFKChannelID AuditLogChangeKey = "afk_channel_id" + AuditLogChangeKeyAFKTimeout AuditLogChangeKey = "afk_timeout" + // AuditLogChangeKeyAllow is sent when a role's permission overwrites changed (stringy int) + AuditLogChangeKeyAllow AuditLogChangeKey = "allow" + AuditLogChangeKeyApplicationID AuditLogChangeKey = "application_id" + // AuditLogChangeKeyArchived is sent when a channel thread is archived/unarchived (bool) + AuditLogChangeKeyArchived AuditLogChangeKey = "archived" + AuditLogChangeKeyAsset AuditLogChangeKey = "asset" + // AuditLogChangeKeyAutoArchiveDuration is sent when a thread's auto archive duration is changed (int) + AuditLogChangeKeyAutoArchiveDuration AuditLogChangeKey = "auto_archive_duration" + AuditLogChangeKeyAvailable AuditLogChangeKey = "available" + AuditLogChangeKeyAvatarHash AuditLogChangeKey = "avatar_hash" + AuditLogChangeKeyBannerHash AuditLogChangeKey = "banner_hash" + AuditLogChangeKeyBitrate AuditLogChangeKey = "bitrate" + AuditLogChangeKeyChannelID AuditLogChangeKey = "channel_id" + AuditLogChangeKeyCode AuditLogChangeKey = "code" + // AuditLogChangeKeyColor is sent when a role's color is changed (int) + AuditLogChangeKeyColor AuditLogChangeKey = "color" + // AuditLogChangeKeyCommunicationDisabledUntil is sent when a user's communication disabled until datetime is changed (stringy ISO8601 datetime) + AuditLogChangeKeyCommunicationDisabledUntil AuditLogChangeKey = "communication_disabled_until" + // AuditLogChangeKeyDeaf is sent when a user is set to be server deafened/undeafened (bool) + AuditLogChangeKeyDeaf AuditLogChangeKey = "deaf" + AuditLogChangeKeyDefaultAutoArchiveDuration AuditLogChangeKey = "default_auto_archive_duration" + AuditLogChangeKeyDefaultMessageNotifications AuditLogChangeKey = "default_message_notifications" + // AuditLogChangeKeyDeny is sent when a role's permission overwrites changed (stringed int) + AuditLogChangeKeyDeny AuditLogChangeKey = "deny" + AuditLogChangeKeyDescription AuditLogChangeKey = "description" + AuditLogChangeKeyDiscoverySplashHash AuditLogChangeKey = "discovery_splash_hash" + AuditLogChangeKeyEnableEmoticons AuditLogChangeKey = "enable_emoticons" + AuditLogChangeKeyEntityType AuditLogChangeKey = "entity_type" + AuditLogChangeKeyExpireBehavior AuditLogChangeKey = "expire_behavior" + AuditLogChangeKeyExpireGracePeriod AuditLogChangeKey = "expire_grace_period" + AuditLogChangeKeyExplicitContentFilter AuditLogChangeKey = "explicit_content_filter" + AuditLogChangeKeyFormatType AuditLogChangeKey = "format_type" + AuditLogChangeKeyGuildID AuditLogChangeKey = "guild_id" + // AuditLogChangeKeyHoist is sent when a role is set to be displayed separately from online members (bool) + AuditLogChangeKeyHoist AuditLogChangeKey = "hoist" + AuditLogChangeKeyIconHash AuditLogChangeKey = "icon_hash" + AuditLogChangeKeyID AuditLogChangeKey = "id" + AuditLogChangeKeyInvitable AuditLogChangeKey = "invitable" + AuditLogChangeKeyInviterID AuditLogChangeKey = "inviter_id" + AuditLogChangeKeyLocation AuditLogChangeKey = "location" + // AuditLogChangeKeyLocked is sent when a channel thread is locked/unlocked (bool) + AuditLogChangeKeyLocked AuditLogChangeKey = "locked" + AuditLogChangeKeyMaxAge AuditLogChangeKey = "max_age" + AuditLogChangeKeyMaxUses AuditLogChangeKey = "max_uses" + // AuditLogChangeKeyMentionable is sent when a role changes its mentionable state (bool) + AuditLogChangeKeyMentionable AuditLogChangeKey = "mentionable" + AuditLogChangeKeyMFALevel AuditLogChangeKey = "mfa_level" + // AuditLogChangeKeyMute is sent when a user is server muted/unmuted (bool) + AuditLogChangeKeyMute AuditLogChangeKey = "mute" + AuditLogChangeKeyName AuditLogChangeKey = "name" + // AuditLogChangeKeyNick is sent when a user's nickname is changed (string) + AuditLogChangeKeyNick AuditLogChangeKey = "nick" + AuditLogChangeKeyNSFW AuditLogChangeKey = "nsfw" + // AuditLogChangeKeyOwnerID is sent when owner id of a guild changed (snowflake.ID) + AuditLogChangeKeyOwnerID AuditLogChangeKey = "owner_id" + // AuditLogChangeKeyPermissionOverwrites is sent when a role's permission overwrites changed (string) + AuditLogChangeKeyPermissionOverwrites AuditLogChangeKey = "permission_overwrites" + // AuditLogChangeKeyPermissions is sent when a role's permissions changed (string) + AuditLogChangeKeyPermissions AuditLogChangeKey = "permissions" + // AuditLogChangeKeyPosition is sent when channel position changed (int) + AuditLogChangeKeyPosition AuditLogChangeKey = "position" + AuditLogChangeKeyPreferredLocale AuditLogChangeKey = "preferred_locale" + AuditLogChangeKeyPrivacyLevel AuditLogChangeKey = "privacy_level" + AuditLogChangeKeyPruneDeleteDays AuditLogChangeKey = "prune_delete_days" + AuditLogChangeKeyPublicUpdatesChannelID AuditLogChangeKey = "public_updates_channel_id" + AuditLogChangeKeyRateLimitPerUser AuditLogChangeKey = "rate_limit_per_user" + AuditLogChangeKeyRegion AuditLogChangeKey = "region" + AuditLogChangeKeyRulesChannelID AuditLogChangeKey = "rules_channel_id" + AuditLogChangeKeySplashHash AuditLogChangeKey = "splash_hash" + AuditLogChangeKeyStatus AuditLogChangeKey = "status" + // AuditLogChangeKeySystemChannelID is sent when system channel id of a guild changed (snowflake.ID) + AuditLogChangeKeySystemChannelID AuditLogChangeKey = "system_channel_id" + AuditLogChangeKeyTags AuditLogChangeKey = "tags" + AuditLogChangeKeyTemporary AuditLogChangeKey = "temporary" + // AuditLogChangeKeyTopic is sent when channel topic changed (string) + AuditLogChangeKeyTopic AuditLogChangeKey = "topic" + AuditLogChangeKeyType AuditLogChangeKey = "type" + AuditLogChangeKeyUnicodeEmoji AuditLogChangeKey = "unicode_emoji" + // AuditLogChangeKeyUserLimit is sent when user limit of a voice channel changed (int) + AuditLogChangeKeyUserLimit AuditLogChangeKey = "user_limit" + AuditLogChangeKeyUses AuditLogChangeKey = "uses" + AuditLogChangeKeyVanityURLCode AuditLogChangeKey = "vanity_url_code" + // AuditLogChangeKeyVerificationLevel is sent when verification level of the server changed (int) + AuditLogChangeKeyVerificationLevel AuditLogChangeKey = "verification_level" + AuditLogChangeKeyWidgetChannelID AuditLogChangeKey = "widget_channel_id" + // AuditLogChangeKeyWidgetEnabled is sent when a server widget is enabled/disabled (bool) + AuditLogChangeKeyWidgetEnabled AuditLogChangeKey = "widget_enabled" + // AuditLogChangeKeyRoleAdd is sent when roles are added to a user (array of discord.PartialRole JSON) + AuditLogChangeKeyRoleAdd AuditLogChangeKey = "$add" + // AuditLogChangeKeyRoleRemove is sent when roles are removed from a user (array of discord.PartialRole JSON) + AuditLogChangeKeyRoleRemove AuditLogChangeKey = "$remove" +) + // AuditLog (https://discord.com/developers/docs/resources/audit-log) These are logs of events that occurred, accessible via the Discord type AuditLog struct { ApplicationCommands []ApplicationCommand `json:"application_commands"` @@ -174,7 +279,7 @@ func (l *AuditLog) UnmarshalJSON(data []byte) error { // AuditLogEntry (https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object) type AuditLogEntry struct { TargetID *snowflake.ID `json:"target_id"` - Changes []AuditLogChangeKey `json:"changes"` + Changes []AuditLogChange `json:"changes"` UserID snowflake.ID `json:"user_id"` ID snowflake.ID `json:"id"` ActionType AuditLogEvent `json:"action_type"` @@ -182,63 +287,25 @@ type AuditLogEntry struct { Reason *string `json:"reason"` } -// AuditLogChangeKey (https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-key) is data representing changes values/settings in an audit log. -type AuditLogChangeKey struct { - Name *string `json:"name"` - Description *string `json:"description"` - IconHash *string `json:"icon_hash"` - SplashHash *string `json:"splash_hash"` - DiscoverySplashHash *string `json:"discovery_splash_hash"` - BannerHash *string `json:"banner_hash"` - OwnerID *snowflake.ID `json:"owner_id"` - Region *string `json:"region"` - PreferredLocale *string `json:"preferred_locale"` - AFKChannelID *snowflake.ID `json:"afk_channel_id"` - AFKTimeout *int `json:"afk_timeout"` - RulesChannelID *snowflake.ID `json:"rules_channel_id"` - PublicUpdatesChannelID *snowflake.ID `json:"public_updates_channel_id"` - MFALevel *MFALevel `json:"mfa_level"` - VerificationLevel *VerificationLevel `json:"verification_level"` - ExplicitContentFilterLevel *ExplicitContentFilterLevel `json:"explicit_content_filter"` - DefaultMessageNotifications *MessageNotificationsLevel `json:"default_message_notifications"` - VanityURLCode *string `json:"vanity_url_code"` - Add []PartialRole `json:"$add"` - Remove []PartialRole `json:"$remove"` - PruneDeleteDays *int `json:"prune_delete_days"` - WidgetEnabled *bool `json:"widget_enabled"` - WidgetChannelID *string `json:"widget_channel_id"` - SystemChannelID *string `json:"system_channel_id"` - Position *int `json:"position"` - Topic *string `json:"topic"` - Bitrate *int `json:"bitrate"` - PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites"` - NSFW *bool `json:"nsfw"` - ApplicationID *snowflake.ID `json:"application_id"` - RateLimitPerUser *int `json:"ratelimit_per_user"` - Permissions *string `json:"permissions"` - Color *int `json:"color"` - Hoist *bool `json:"hoist"` - Mentionable *bool `json:"mentionable"` - Allow *Permissions `json:"allow"` - Deny *Permissions `json:"deny"` - Code *string `json:"code"` - ChannelID *snowflake.ID `json:"channel_id"` - InviterID *snowflake.ID `json:"inviter_id"` - MaxUses *int `json:"max_uses"` - Uses *int `json:"uses"` - MaxAge *string `json:"max_age"` - Temporary *bool `json:"temporary"` - Deaf *bool `json:"deaf"` - Mute *bool `json:"mute"` - Nick *string `json:"nick"` - AvatarHash *string `json:"avatar_hash"` - ID *snowflake.ID `json:"id"` - Type any `json:"type"` - EnableEmoticons *bool `json:"enable_emoticons"` - ExpireBehavior *IntegrationExpireBehavior `json:"expire_behavior"` - ExpireGracePeriod *int `json:"expire_grace_period"` - UserLimit *int `json:"user_limit"` - PrivacyLevel *StagePrivacyLevel `json:"privacy_level"` +// AuditLogChange (https://discord.com/developers/docs/resources/audit-log#audit-log-change-object) contains what was changed. +// For a list of possible keys & values see the discord documentation. +type AuditLogChange struct { + // NewValue is the new value of the key after the change as a json.RawMessage. + NewValue json.RawMessage `json:"new_value"` + // OldValue is the old value of the key before the change as a json.RawMessage. + OldValue json.RawMessage `json:"old_value"` + // Key is the key of the change. + Key AuditLogChangeKey `json:"key"` +} + +// UnmarshalNewValue unmarshals the NewValue field into the provided type. +func (c *AuditLogChange) UnmarshalNewValue(v any) error { + return json.Unmarshal(c.NewValue, v) +} + +// UnmarshalOldValue unmarshals the OldValue field into the provided type. +func (c *AuditLogChange) UnmarshalOldValue(v any) error { + return json.Unmarshal(c.OldValue, v) } // OptionalAuditLogEntryInfo (https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info) diff --git a/discord/auto_moderation.go b/discord/auto_moderation.go index 83a2c77d6..4ac8d5056 100644 --- a/discord/auto_moderation.go +++ b/discord/auto_moderation.go @@ -10,6 +10,7 @@ type AutoModerationEventType int const ( AutoModerationEventTypeMessageSend AutoModerationEventType = iota + 1 + AutoModerationEventTypeMemberUpdate ) type AutoModerationTriggerType int @@ -20,6 +21,7 @@ const ( AutoModerationTriggerTypeSpam AutoModerationTriggerTypeKeywordPresent AutoModerationTriggerTypeMentionSpam + AutoModerationTriggerTypeMemberProfile ) type AutoModerationTriggerMetadata struct { @@ -45,6 +47,7 @@ const ( AutoModerationActionTypeBlockMessage AutoModerationActionType = iota + 1 AutoModerationActionTypeSendAlertMessage AutoModerationActionTypeTimeout + AutoModerationActionTypeBlockMemberInteraction ) type AutoModerationAction struct { diff --git a/discord/ban.go b/discord/ban.go index c730ba5a5..b8c9b92ec 100644 --- a/discord/ban.go +++ b/discord/ban.go @@ -1,5 +1,7 @@ package discord +import "github.com/disgoorg/snowflake/v2" + // Ban represents a banned User from a Guild (https://discord.com/developers/docs/resources/guild#ban-object) type Ban struct { Reason *string `json:"reason,omitempty"` @@ -10,3 +12,15 @@ type Ban struct { type AddBan struct { DeleteMessageSeconds int `json:"delete_message_seconds,omitempty"` } + +// BulkBan is used to bulk ban Users +type BulkBan struct { + UserIDs []snowflake.ID `json:"user_ids"` + DeleteMessageSeconds int `json:"delete_message_seconds,omitempty"` +} + +// BulkBanResult is the result of a BulkBan request +type BulkBanResult struct { + BannedUsers []snowflake.ID `json:"banned_users"` + FailedUsers []snowflake.ID `json:"failed_users"` +} diff --git a/discord/cdn_endpoints.go b/discord/cdn_endpoints.go index 8186af5a6..a36598ac1 100644 --- a/discord/cdn_endpoints.go +++ b/discord/cdn_endpoints.go @@ -4,7 +4,10 @@ import ( "strings" ) -const CDN = "https://cdn.discordapp.com" +const ( + CDN = "https://cdn.discordapp.com" + CDNMedia = "https://media.discordapp.net" +) var ( CustomEmoji = NewCDN("/emojis/{emote.id}", FileFormatPNG, FileFormatGIF) @@ -14,6 +17,8 @@ var ( GuildDiscoverySplash = NewCDN("/discovery-splashes/{guild.id}/{guild.discovery.splash.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP) GuildBanner = NewCDN("/banners/{guild.id}/{guild.banner.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP, FileFormatGIF) + GuildScheduledEventCover = NewCDN("/guild-events/{event.id}/{event.cover.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP) + RoleIcon = NewCDN("/role-icons/{role.id}/{role.icon.hash}", FileFormatPNG, FileFormatJPEG) UserBanner = NewCDN("/banners/{user.id}/{user.banner.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP, FileFormatGIF) @@ -23,8 +28,9 @@ var ( ChannelIcon = NewCDN("/channel-icons/{channel.id}/{channel.icon.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP) MemberAvatar = NewCDN("/guilds/{guild.id}/users/{user.id}/avatars/{member.avatar.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP, FileFormatGIF) + MemberBanner = NewCDN("/guilds/{guild.id}/users/{user.id}/banners/{member.avatar.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP, FileFormatGIF) - UserAvatarDecoration = NewCDN("/avatar-decorations/{user.id}/{user.avatar.decoration.hash}", FileFormatPNG) + AvatarDecoration = NewCDN("/avatar-decoration-presets/{user.avatar.decoration.hash}", FileFormatPNG) ApplicationIcon = NewCDN("/app-icons/{application.id}/{icon.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP) ApplicationCover = NewCDN("/app-assets/{application.id}/{cover.image.hash}", FileFormatPNG, FileFormatJPEG, FileFormatWebP) @@ -40,6 +46,8 @@ var ( CustomSticker = NewCDN("/stickers/{sticker.id}", FileFormatPNG, FileFormatLottie, FileFormatGIF) AttachmentFile = NewCDN("/attachments/{channel.id}/{attachment.id}/{file.name}", FileFormatNone) + + SoundboardSoundFile = NewCDN("/soundboard-sounds/{sound.id}", FileFormatNone) ) // FileFormat is the type of file on Discord's CDN (https://discord.com/developers/docs/reference#image-formatting-image-formats) @@ -87,12 +95,23 @@ func (e CDNEndpoint) URL(format FileFormat, values QueryValues, params ...any) s if query != "" { query = "?" + query } - return urlPrint(CDN+e.Route+"."+format.String(), params...) + query + + // for some reason custom gif stickers use a different cdn url, blame discord for this one + if format == FileFormatGIF && e.Route == "/stickers/{sticker.id}" { + return urlPrint(CDNMedia+e.Route+"."+format.String(), params...) + query + } + route := CDN + e.Route + // only append period and file extension if the format is not FileFormatNone + if format != FileFormatNone { + route += "." + format.String() + } + + return urlPrint(route, params...) + query } -func DefaultCDNConfig() *CDNConfig { +func DefaultCDNConfig(format FileFormat) *CDNConfig { return &CDNConfig{ - Format: FileFormatPNG, + Format: format, Values: QueryValues{}, } } @@ -124,7 +143,13 @@ func WithFormat(format FileFormat) CDNOpt { } func formatAssetURL(cdnRoute *CDNEndpoint, opts []CDNOpt, params ...any) string { - config := DefaultCDNConfig() + format := FileFormatNone + if len(cdnRoute.Formats) > 0 { // just in case someone fucks up + // use the first provided format in the route definition itself. if the user provides a different format, this will be overriden by the Apply function call below + // previously, the default format was png, which would cause issues for cdn endpoints like attachments and soundboard sounds, requiring custom "overrides" + format = cdnRoute.Formats[0] + } + config := DefaultCDNConfig(format) config.Apply(opts) var lastStringParam string @@ -141,7 +166,8 @@ func formatAssetURL(cdnRoute *CDNEndpoint, opts []CDNOpt, params ...any) string lastStringParam = *ptrStr } - if strings.HasPrefix(lastStringParam, "a_") && !config.Format.Animated() { + // some endpoints have a_ prefix for animated images except the AvatarDecoration endpoint does not like this + if strings.HasPrefix(lastStringParam, "a_") && !config.Format.Animated() && cdnRoute.Route != "/avatar-decoration-presets/{user.avatar.decoration.hash}" { config.Format = FileFormatGIF } diff --git a/discord/channel.go b/discord/channel.go index 4b0e9caf6..fe32139ee 100644 --- a/discord/channel.go +++ b/discord/channel.go @@ -185,6 +185,11 @@ func (u *UnmarshalChannel) UnmarshalJSON(data []byte) error { err = json.Unmarshal(data, &v) channel = v + case ChannelTypeGroupDM: + var v GroupDMChannel + err = json.Unmarshal(data, &v) + channel = v + case ChannelTypeGuildCategory: var v GuildCategoryChannel err = json.Unmarshal(data, &v) @@ -423,6 +428,91 @@ func (c DMChannel) CreatedAt() time.Time { func (DMChannel) channel() {} func (DMChannel) messageChannel() {} +var ( + _ Channel = (*GroupDMChannel)(nil) + _ MessageChannel = (*GroupDMChannel)(nil) +) + +type GroupDMChannel struct { + id snowflake.ID + ownerID *snowflake.ID + name string + lastPinTimestamp *time.Time + lastMessageID *snowflake.ID + icon *string +} + +func (c *GroupDMChannel) UnmarshalJSON(data []byte) error { + var v groupDMChannel + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + c.id = v.ID + c.ownerID = v.OwnerID + c.name = v.Name + c.lastPinTimestamp = v.LastPinTimestamp + c.lastMessageID = v.LastMessageID + c.icon = v.Icon + return nil +} + +func (c GroupDMChannel) MarshalJSON() ([]byte, error) { + return json.Marshal(groupDMChannel{ + ID: c.id, + Type: c.Type(), + OwnerID: c.ownerID, + Name: c.name, + LastPinTimestamp: c.lastPinTimestamp, + LastMessageID: c.lastMessageID, + Icon: c.icon, + }) +} + +func (c GroupDMChannel) String() string { + return channelString(c) +} + +func (c GroupDMChannel) ID() snowflake.ID { + return c.id +} + +func (GroupDMChannel) Type() ChannelType { + return ChannelTypeGroupDM +} + +func (c GroupDMChannel) OwnerID() *snowflake.ID { + return c.ownerID +} + +func (c GroupDMChannel) Name() string { + return c.name +} + +func (c GroupDMChannel) LastPinTimestamp() *time.Time { + return c.lastPinTimestamp +} + +func (c GroupDMChannel) LastMessageID() *snowflake.ID { + return c.lastMessageID +} + +func (c GroupDMChannel) CreatedAt() time.Time { + return c.id.Time() +} + +// IconURL returns the icon URL of this group DM or nil if not set +func (c GroupDMChannel) IconURL(opts ...CDNOpt) *string { + if c.icon == nil { + return nil + } + url := formatAssetURL(ChannelIcon, opts, c.id, *c.icon) + return &url +} + +func (GroupDMChannel) channel() {} +func (GroupDMChannel) messageChannel() {} + var ( _ Channel = (*GuildVoiceChannel)(nil) _ GuildChannel = (*GuildVoiceChannel)(nil) @@ -431,22 +521,19 @@ var ( ) type GuildVoiceChannel struct { - id snowflake.ID - guildID snowflake.ID - position int - permissionOverwrites []PermissionOverwrite - name string - bitrate int - UserLimit int - parentID *snowflake.ID - rtcRegion string - VideoQualityMode VideoQualityMode - lastMessageID *snowflake.ID - lastPinTimestamp *time.Time - topic *string - nsfw bool - defaultAutoArchiveDuration AutoArchiveDuration - rateLimitPerUser int + id snowflake.ID + guildID snowflake.ID + position int + permissionOverwrites []PermissionOverwrite + name string + bitrate int + UserLimit int + parentID *snowflake.ID + rtcRegion string + VideoQualityMode VideoQualityMode + lastMessageID *snowflake.ID + nsfw bool + rateLimitPerUser int } func (c *GuildVoiceChannel) UnmarshalJSON(data []byte) error { @@ -466,33 +553,27 @@ func (c *GuildVoiceChannel) UnmarshalJSON(data []byte) error { c.rtcRegion = v.RTCRegion c.VideoQualityMode = v.VideoQualityMode c.lastMessageID = v.LastMessageID - c.lastPinTimestamp = v.LastPinTimestamp - c.topic = v.Topic c.nsfw = v.NSFW - c.defaultAutoArchiveDuration = v.DefaultAutoArchiveDuration c.rateLimitPerUser = v.RateLimitPerUser return nil } func (c GuildVoiceChannel) MarshalJSON() ([]byte, error) { return json.Marshal(guildVoiceChannel{ - ID: c.id, - Type: c.Type(), - GuildID: c.guildID, - Position: c.position, - PermissionOverwrites: c.permissionOverwrites, - Name: c.name, - Bitrate: c.bitrate, - UserLimit: c.UserLimit, - ParentID: c.parentID, - RTCRegion: c.rtcRegion, - VideoQualityMode: c.VideoQualityMode, - LastMessageID: c.lastMessageID, - LastPinTimestamp: c.lastPinTimestamp, - Topic: c.topic, - NSFW: c.nsfw, - DefaultAutoArchiveDuration: c.defaultAutoArchiveDuration, - RateLimitPerUser: c.rateLimitPerUser, + ID: c.id, + Type: c.Type(), + GuildID: c.guildID, + Position: c.position, + PermissionOverwrites: c.permissionOverwrites, + Name: c.name, + Bitrate: c.bitrate, + UserLimit: c.UserLimit, + ParentID: c.parentID, + RTCRegion: c.rtcRegion, + VideoQualityMode: c.VideoQualityMode, + LastMessageID: c.lastMessageID, + NSFW: c.nsfw, + RateLimitPerUser: c.rateLimitPerUser, }) } @@ -544,20 +625,23 @@ func (c GuildVoiceChannel) LastMessageID() *snowflake.ID { return c.lastMessageID } +// LastPinTimestamp always returns nil for GuildVoiceChannel(s) as they cannot have pinned messages. func (c GuildVoiceChannel) LastPinTimestamp() *time.Time { - return c.lastPinTimestamp + return nil } +// Topic always returns nil for GuildVoiceChannel(s) as they do not have their own topic. func (c GuildVoiceChannel) Topic() *string { - return c.topic + return nil } func (c GuildVoiceChannel) NSFW() bool { return c.nsfw } +// DefaultAutoArchiveDuration is always 0 for GuildVoiceChannel(s) as they do not have their own AutoArchiveDuration. func (c GuildVoiceChannel) DefaultAutoArchiveDuration() AutoArchiveDuration { - return c.defaultAutoArchiveDuration + return 0 } func (c GuildVoiceChannel) RateLimitPerUser() int { @@ -930,9 +1014,10 @@ func (GuildThread) messageChannel() {} func (GuildThread) guildMessageChannel() {} var ( - _ Channel = (*GuildStageVoiceChannel)(nil) - _ GuildChannel = (*GuildStageVoiceChannel)(nil) - _ GuildAudioChannel = (*GuildStageVoiceChannel)(nil) + _ Channel = (*GuildStageVoiceChannel)(nil) + _ GuildChannel = (*GuildStageVoiceChannel)(nil) + _ GuildAudioChannel = (*GuildStageVoiceChannel)(nil) + _ GuildMessageChannel = (*GuildStageVoiceChannel)(nil) ) type GuildStageVoiceChannel struct { @@ -944,6 +1029,10 @@ type GuildStageVoiceChannel struct { bitrate int parentID *snowflake.ID rtcRegion string + VideoQualityMode VideoQualityMode + lastMessageID *snowflake.ID + nsfw bool + rateLimitPerUser int } func (c *GuildStageVoiceChannel) UnmarshalJSON(data []byte) error { @@ -960,6 +1049,10 @@ func (c *GuildStageVoiceChannel) UnmarshalJSON(data []byte) error { c.bitrate = v.Bitrate c.parentID = v.ParentID c.rtcRegion = v.RTCRegion + c.VideoQualityMode = v.VideoQualityMode + c.lastMessageID = v.LastMessageID + c.nsfw = v.NSFW + c.rateLimitPerUser = v.RateLimitPerUser return nil } @@ -974,6 +1067,10 @@ func (c GuildStageVoiceChannel) MarshalJSON() ([]byte, error) { Bitrate: c.bitrate, ParentID: c.parentID, RTCRegion: c.rtcRegion, + VideoQualityMode: c.VideoQualityMode, + LastMessageID: c.lastMessageID, + NSFW: c.nsfw, + RateLimitPerUser: c.rateLimitPerUser, }) } @@ -1021,13 +1118,42 @@ func (c GuildStageVoiceChannel) ParentID() *snowflake.ID { return c.parentID } +func (c GuildStageVoiceChannel) LastMessageID() *snowflake.ID { + return c.lastMessageID +} + +// LastPinTimestamp always returns nil for GuildStageVoiceChannel(s) as they cannot have pinned messages. +func (c GuildStageVoiceChannel) LastPinTimestamp() *time.Time { + return nil +} + +// Topic always returns nil for GuildStageVoiceChannel(s) as they do not have their own topic. +func (c GuildStageVoiceChannel) Topic() *string { + return nil +} + +func (c GuildStageVoiceChannel) NSFW() bool { + return c.nsfw +} + +// DefaultAutoArchiveDuration is always 0 for GuildStageVoiceChannel(s) as they do not have their own AutoArchiveDuration. +func (c GuildStageVoiceChannel) DefaultAutoArchiveDuration() AutoArchiveDuration { + return 0 +} + +func (c GuildStageVoiceChannel) RateLimitPerUser() int { + return c.rateLimitPerUser +} + func (c GuildStageVoiceChannel) CreatedAt() time.Time { return c.id.Time() } -func (GuildStageVoiceChannel) channel() {} -func (GuildStageVoiceChannel) guildChannel() {} -func (GuildStageVoiceChannel) guildAudioChannel() {} +func (GuildStageVoiceChannel) channel() {} +func (GuildStageVoiceChannel) messageChannel() {} +func (GuildStageVoiceChannel) guildChannel() {} +func (GuildStageVoiceChannel) guildAudioChannel() {} +func (GuildStageVoiceChannel) guildMessageChannel() {} var ( _ Channel = (*GuildForumChannel)(nil) @@ -1272,7 +1398,7 @@ type PartialChannel struct { type VideoQualityMode int const ( - VideoQualityModeAuto = iota + 1 + VideoQualityModeAuto VideoQualityMode = iota + 1 VideoQualityModeFull ) @@ -1386,9 +1512,6 @@ func ApplyLastPinTimestampToChannel(channel GuildMessageChannel, lastPinTimestam case GuildTextChannel: c.lastPinTimestamp = lastPinTimestamp return c - case GuildVoiceChannel: - c.lastPinTimestamp = lastPinTimestamp - return c case GuildNewsChannel: c.lastPinTimestamp = lastPinTimestamp return c diff --git a/discord/channel_create.go b/discord/channel_create.go index eb5ac7d21..63e99b2f3 100644 --- a/discord/channel_create.go +++ b/discord/channel_create.go @@ -22,14 +22,15 @@ var ( ) type GuildTextChannelCreate struct { - Name string `json:"name"` - Topic string `json:"topic,omitempty"` - RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` - Position int `json:"position,omitempty"` - PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` - ParentID snowflake.ID `json:"parent_id,omitempty"` - NSFW bool `json:"nsfw,omitempty"` - DefaultAutoArchiveDuration AutoArchiveDuration `json:"default_auto_archive_days,omitempty"` + Name string `json:"name"` + Topic string `json:"topic,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` + Position int `json:"position,omitempty"` + PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID snowflake.ID `json:"parent_id,omitempty"` + NSFW bool `json:"nsfw,omitempty"` + DefaultAutoArchiveDuration AutoArchiveDuration `json:"default_auto_archive_days,omitempty"` + DefaultThreadRateLimitPerUser int `json:"default_thread_rate_limit_per_user,omitempty"` } func (c GuildTextChannelCreate) Type() ChannelType { @@ -57,12 +58,15 @@ var ( type GuildVoiceChannelCreate struct { Name string `json:"name"` - Topic string `json:"topic,omitempty"` Bitrate int `json:"bitrate,omitempty"` UserLimit int `json:"user_limit,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` Position int `json:"position,omitempty"` PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` ParentID snowflake.ID `json:"parent_id,omitempty"` + NSFW bool `json:"nsfw,omitempty"` + RTCRegion string `json:"rtc_region,omitempty"` + VideoQualityMode VideoQualityMode `json:"video_quality_mode,omitempty"` } func (c GuildVoiceChannelCreate) Type() ChannelType { @@ -119,14 +123,15 @@ var ( ) type GuildNewsChannelCreate struct { - Name string `json:"name"` - Topic string `json:"topic,omitempty"` - RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` - Position int `json:"position,omitempty"` - PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` - ParentID snowflake.ID `json:"parent_id,omitempty"` - NSFW bool `json:"nsfw,omitempty"` - DefaultAutoArchiveDuration AutoArchiveDuration `json:"default_auto_archive_days,omitempty"` + Name string `json:"name"` + Topic string `json:"topic,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` + Position int `json:"position,omitempty"` + PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID snowflake.ID `json:"parent_id,omitempty"` + NSFW bool `json:"nsfw,omitempty"` + DefaultAutoArchiveDuration AutoArchiveDuration `json:"default_auto_archive_days,omitempty"` + DefaultThreadRateLimitPerUser int `json:"default_thread_rate_limit_per_user,omitempty"` } func (c GuildNewsChannelCreate) Type() ChannelType { @@ -154,12 +159,15 @@ var ( type GuildStageVoiceChannelCreate struct { Name string `json:"name"` - Topic string `json:"topic,omitempty"` Bitrate int `json:"bitrate,omitempty"` UserLimit int `json:"user_limit,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` Position int `json:"position,omitempty"` PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` ParentID snowflake.ID `json:"parent_id,omitempty"` + NSFW bool `json:"nsfw,omitempty"` + RTCRegion string `json:"rtc_region,omitempty"` + VideoQualityMode VideoQualityMode `json:"video_quality_mode,omitempty"` } func (c GuildStageVoiceChannelCreate) Type() ChannelType { @@ -181,16 +189,17 @@ func (GuildStageVoiceChannelCreate) channelCreate() {} func (GuildStageVoiceChannelCreate) guildChannelCreate() {} type GuildForumChannelCreate struct { - Name string `json:"name"` - Topic string `json:"topic,omitempty"` - Position int `json:"position,omitempty"` - PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` - ParentID snowflake.ID `json:"parent_id,omitempty"` - RateLimitPerUser int `json:"rate_limit_per_user"` - DefaultReactionEmoji DefaultReactionEmoji `json:"default_reaction_emoji"` - AvailableTags []ChannelTag `json:"available_tags"` - DefaultSortOrder DefaultSortOrder `json:"default_sort_order"` - DefaultForumLayout DefaultForumLayout `json:"default_forum_layout"` + Name string `json:"name"` + Topic string `json:"topic,omitempty"` + Position int `json:"position,omitempty"` + PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID snowflake.ID `json:"parent_id,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` + DefaultReactionEmoji DefaultReactionEmoji `json:"default_reaction_emoji"` + AvailableTags []ChannelTag `json:"available_tags"` + DefaultSortOrder DefaultSortOrder `json:"default_sort_order"` + DefaultForumLayout DefaultForumLayout `json:"default_forum_layout"` + DefaultThreadRateLimitPerUser int `json:"default_thread_rate_limit_per_user,omitempty"` } func (c GuildForumChannelCreate) Type() ChannelType { @@ -212,15 +221,16 @@ func (GuildForumChannelCreate) channelCreate() {} func (GuildForumChannelCreate) guildChannelCreate() {} type GuildMediaChannelCreate struct { - Name string `json:"name"` - Topic string `json:"topic,omitempty"` - Position int `json:"position,omitempty"` - PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` - ParentID snowflake.ID `json:"parent_id,omitempty"` - RateLimitPerUser int `json:"rate_limit_per_user"` - DefaultReactionEmoji DefaultReactionEmoji `json:"default_reaction_emoji"` - AvailableTags []ChannelTag `json:"available_tags"` - DefaultSortOrder DefaultSortOrder `json:"default_sort_order"` + Name string `json:"name"` + Topic string `json:"topic,omitempty"` + Position int `json:"position,omitempty"` + PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID snowflake.ID `json:"parent_id,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` + DefaultReactionEmoji DefaultReactionEmoji `json:"default_reaction_emoji"` + AvailableTags []ChannelTag `json:"available_tags"` + DefaultSortOrder DefaultSortOrder `json:"default_sort_order"` + DefaultThreadRateLimitPerUser int `json:"default_thread_rate_limit_per_user,omitempty"` } func (c GuildMediaChannelCreate) Type() ChannelType { diff --git a/discord/channel_update.go b/discord/channel_update.go index 9ddfe4d2e..975c78d0f 100644 --- a/discord/channel_update.go +++ b/discord/channel_update.go @@ -39,6 +39,7 @@ type GuildVoiceChannelUpdate struct { PermissionOverwrites *[]PermissionOverwrite `json:"permission_overwrites,omitempty"` ParentID *snowflake.ID `json:"parent_id,omitempty"` RTCRegion *string `json:"rtc_region,omitempty"` + NSFW *bool `json:"nsfw,omitempty"` VideoQualityMode *VideoQualityMode `json:"video_quality_mode,omitempty"` } @@ -83,7 +84,7 @@ func (GuildThreadUpdate) guildChannelUpdate() {} type GuildStageVoiceChannelUpdate struct { Name *string `json:"name,omitempty"` Position *int `json:"position,omitempty"` - Topic *string `json:"topic,omitempty"` + RateLimitPerUser *int `json:"rate_limit_per_user,omitempty"` Bitrate *int `json:"bitrate,omitempty"` UserLimit *int `json:"user_limit,omitempty"` PermissionOverwrites *[]PermissionOverwrite `json:"permission_overwrites,omitempty"` diff --git a/discord/channels_raw.go b/discord/channels_raw.go index 33f75e8a3..1dba72b2f 100644 --- a/discord/channels_raw.go +++ b/discord/channels_raw.go @@ -15,6 +15,16 @@ type dmChannel struct { LastPinTimestamp *time.Time `json:"last_pin_timestamp"` } +type groupDMChannel struct { + ID snowflake.ID `json:"id"` + Type ChannelType `json:"type"` + OwnerID *snowflake.ID `json:"owner_id"` + Name string `json:"name"` + LastPinTimestamp *time.Time `json:"last_pin_timestamp"` + LastMessageID *snowflake.ID `json:"last_message_id"` + Icon *string `json:"icon"` +} + type guildTextChannel struct { ID snowflake.ID `json:"id"` Type ChannelType `json:"type"` @@ -117,23 +127,20 @@ func (t *guildCategoryChannel) UnmarshalJSON(data []byte) error { } type guildVoiceChannel struct { - ID snowflake.ID `json:"id"` - Type ChannelType `json:"type"` - GuildID snowflake.ID `json:"guild_id"` - Position int `json:"position"` - PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites"` - Name string `json:"name"` - Bitrate int `json:"bitrate"` - UserLimit int `json:"user_limit"` - ParentID *snowflake.ID `json:"parent_id"` - RTCRegion string `json:"rtc_region"` - VideoQualityMode VideoQualityMode `json:"video_quality_mode"` - LastMessageID *snowflake.ID `json:"last_message_id"` - LastPinTimestamp *time.Time `json:"last_pin_timestamp"` - Topic *string `json:"topic"` - NSFW bool `json:"nsfw"` - DefaultAutoArchiveDuration AutoArchiveDuration `json:"default_auto_archive_duration"` - RateLimitPerUser int `json:"rate_limit_per_user"` + ID snowflake.ID `json:"id"` + Type ChannelType `json:"type"` + GuildID snowflake.ID `json:"guild_id"` + Position int `json:"position"` + PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites"` + Name string `json:"name"` + Bitrate int `json:"bitrate"` + UserLimit int `json:"user_limit"` + ParentID *snowflake.ID `json:"parent_id"` + RTCRegion string `json:"rtc_region"` + VideoQualityMode VideoQualityMode `json:"video_quality_mode"` + LastMessageID *snowflake.ID `json:"last_message_id"` + NSFW bool `json:"nsfw"` + RateLimitPerUser int `json:"rate_limit_per_user"` } func (t *guildVoiceChannel) UnmarshalJSON(data []byte) error { @@ -157,9 +164,14 @@ type guildStageVoiceChannel struct { Position int `json:"position"` PermissionOverwrites []PermissionOverwrite `json:"permission_overwrites"` Name string `json:"name"` - Bitrate int `json:"bitrate,"` + Bitrate int `json:"bitrate"` + UserLimit int `json:"user_limit"` ParentID *snowflake.ID `json:"parent_id"` RTCRegion string `json:"rtc_region"` + VideoQualityMode VideoQualityMode `json:"video_quality_mode"` + LastMessageID *snowflake.ID `json:"last_message_id"` + NSFW bool `json:"nsfw"` + RateLimitPerUser int `json:"rate_limit_per_user"` } func (t *guildStageVoiceChannel) UnmarshalJSON(data []byte) error { diff --git a/discord/component.go b/discord/component.go index ff4742d38..f43611e74 100644 --- a/discord/component.go +++ b/discord/component.go @@ -12,7 +12,7 @@ type ComponentType int // Supported ComponentType(s) const ( - ComponentTypeActionRow = iota + 1 + ComponentTypeActionRow ComponentType = iota + 1 ComponentTypeButton ComponentTypeStringSelectMenu ComponentTypeTextInput @@ -229,46 +229,76 @@ type ButtonStyle int // Supported ButtonStyle(s) const ( - ButtonStylePrimary = iota + 1 + ButtonStylePrimary ButtonStyle = iota + 1 ButtonStyleSecondary ButtonStyleSuccess ButtonStyleDanger ButtonStyleLink + ButtonStylePremium ) // NewButton creates a new ButtonComponent with the provided parameters. Link ButtonComponent(s) need a URL and other ButtonComponent(s) need a customID -func NewButton(style ButtonStyle, label string, customID string, url string) ButtonComponent { +func NewButton(style ButtonStyle, label string, customID string, url string, skuID snowflake.ID) ButtonComponent { return ButtonComponent{ Style: style, CustomID: customID, URL: url, Label: label, + SkuID: skuID, } } // NewPrimaryButton creates a new ButtonComponent with ButtonStylePrimary & the provided parameters func NewPrimaryButton(label string, customID string) ButtonComponent { - return NewButton(ButtonStylePrimary, label, customID, "") + return ButtonComponent{ + Style: ButtonStylePrimary, + Label: label, + CustomID: customID, + } } // NewSecondaryButton creates a new ButtonComponent with ButtonStyleSecondary & the provided parameters func NewSecondaryButton(label string, customID string) ButtonComponent { - return NewButton(ButtonStyleSecondary, label, customID, "") + return ButtonComponent{ + Style: ButtonStyleSecondary, + Label: label, + CustomID: customID, + } } // NewSuccessButton creates a new ButtonComponent with ButtonStyleSuccess & the provided parameters func NewSuccessButton(label string, customID string) ButtonComponent { - return NewButton(ButtonStyleSuccess, label, customID, "") + return ButtonComponent{ + Style: ButtonStyleSuccess, + Label: label, + CustomID: customID, + } } // NewDangerButton creates a new ButtonComponent with ButtonStyleDanger & the provided parameters func NewDangerButton(label string, customID string) ButtonComponent { - return NewButton(ButtonStyleDanger, label, customID, "") + return ButtonComponent{ + Style: ButtonStyleDanger, + Label: label, + CustomID: customID, + } } // NewLinkButton creates a new link ButtonComponent with ButtonStyleLink & the provided parameters func NewLinkButton(label string, url string) ButtonComponent { - return NewButton(ButtonStyleLink, label, "", url) + return ButtonComponent{ + Style: ButtonStyleLink, + Label: label, + URL: url, + } +} + +// NewPremiumButton creates a new ButtonComponent with ButtonStylePremium & the provided parameters +func NewPremiumButton(skuID snowflake.ID) ButtonComponent { + return ButtonComponent{ + Style: ButtonStylePremium, + SkuID: skuID, + } } var ( @@ -281,6 +311,7 @@ type ButtonComponent struct { Label string `json:"label,omitempty"` Emoji *ComponentEmoji `json:"emoji,omitempty"` CustomID string `json:"custom_id,omitempty"` + SkuID snowflake.ID `json:"sku_id,omitempty"` URL string `json:"url,omitempty"` Disabled bool `json:"disabled,omitempty"` } @@ -342,6 +373,12 @@ func (c ButtonComponent) WithURL(url string) ButtonComponent { return c } +// WithSkuID returns a new ButtonComponent with the provided skuID +func (c ButtonComponent) WithSkuID(skuID snowflake.ID) ButtonComponent { + c.SkuID = skuID + return c +} + // AsEnabled returns a new ButtonComponent but enabled func (c ButtonComponent) AsEnabled() ButtonComponent { c.Disabled = false @@ -459,6 +496,6 @@ func (c TextInputComponent) WithValue(value string) TextInputComponent { type TextInputStyle int const ( - TextInputStyleShort = iota + 1 + TextInputStyleShort TextInputStyle = iota + 1 TextInputStyleParagraph ) diff --git a/discord/connection.go b/discord/connection.go index 771d52b7e..eba27ce9c 100644 --- a/discord/connection.go +++ b/discord/connection.go @@ -15,23 +15,30 @@ type Connection struct { type ConnectionType string const ( + ConnectionTypeAmazonMusic ConnectionType = "amazon-music" ConnectionTypeBattleNet ConnectionType = "battlenet" + ConnectionTypeBluesky ConnectionType = "bluesky" + ConnectionTypeBungie ConnectionType = "bungie" + ConnectionTypeCrunchyroll ConnectionType = "crunchyroll" + ConnectionTypeDomain ConnectionType = "domain" ConnectionTypeEbay ConnectionType = "ebay" ConnectionTypeEpicGames ConnectionType = "epicgames" ConnectionTypeFacebook ConnectionType = "facebook" ConnectionTypeGitHub ConnectionType = "github" ConnectionTypeInstagram ConnectionType = "instagram" ConnectionTypeLeagueOfLegends ConnectionType = "leagueoflegends" + ConnectionTypeMastodon ConnectionType = "mastodon" ConnectionTypePayPal ConnectionType = "paypal" ConnectionTypePlayStationNetwork ConnectionType = "playstation" ConnectionTypeReddit ConnectionType = "reddit" ConnectionTypeRiotGames ConnectionType = "riotgames" + ConnectionTypeRoblox ConnectionType = "roblox" ConnectionTypeSpotify ConnectionType = "spotify" ConnectionTypeSkype ConnectionType = "skype" ConnectionTypeSteam ConnectionType = "steam" ConnectionTypeTikTok ConnectionType = "tiktok" ConnectionTypeTwitch ConnectionType = "twitch" - ConnectionTypeTwitter ConnectionType = "twitter" + ConnectionTypeX ConnectionType = "twitter" ConnectionTypeXbox ConnectionType = "xbox" ConnectionTypeYouTube ConnectionType = "youtube" ) diff --git a/discord/embed.go b/discord/embed.go index 3de752ff1..30512902f 100644 --- a/discord/embed.go +++ b/discord/embed.go @@ -7,12 +7,14 @@ type EmbedType string // Constants for EmbedType const ( - EmbedTypeRich EmbedType = "rich" - EmbedTypeImage EmbedType = "image" - EmbedTypeVideo EmbedType = "video" - EmbedTypeGifV EmbedType = "rich" - EmbedTypeArticle EmbedType = "article" - EmbedTypeLink EmbedType = "link" + EmbedTypeRich EmbedType = "rich" + EmbedTypeImage EmbedType = "image" + EmbedTypeVideo EmbedType = "video" + EmbedTypeGifV EmbedType = "gifv" + EmbedTypeArticle EmbedType = "article" + EmbedTypeLink EmbedType = "link" + EmbedTypeAutoModerationMessage EmbedType = "auto_moderation_message" + EmbedTypePollResult EmbedType = "poll_result" ) // Embed allows you to send embeds to discord @@ -32,6 +34,25 @@ type Embed struct { Fields []EmbedField `json:"fields,omitempty"` } +func (e Embed) FindField(fieldFindFunc func(field EmbedField) bool) (EmbedField, bool) { + for _, field := range e.Fields { + if fieldFindFunc(field) { + return field, true + } + } + return EmbedField{}, false +} + +func (e Embed) FindAllFields(fieldFindFunc func(field EmbedField) bool) []EmbedField { + var fields []EmbedField + for _, field := range e.Fields { + if fieldFindFunc(field) { + fields = append(fields, field) + } + } + return fields +} + // The EmbedResource of an Embed.Image/Embed.Thumbnail/Embed.Video type EmbedResource struct { URL string `json:"url,omitempty"` @@ -67,3 +88,16 @@ type EmbedField struct { Value string `json:"value"` Inline *bool `json:"inline,omitempty"` } + +type EmbedFieldPollResult string + +const ( + EmbedFieldPollResultQuestionText EmbedFieldPollResult = "poll_question_text" + EmbedFieldPollResultVictorAnswerVotes EmbedFieldPollResult = "victor_answer_votes" + EmbedFieldPollResultTotalVotes EmbedFieldPollResult = "total_votes" + EmbedFieldPollResultVictorAnswerID EmbedFieldPollResult = "victor_answer_id" + EmbedFieldPollResultVictorAnswerText EmbedFieldPollResult = "victor_answer_text" + EmbedFieldPollResultVictorAnswerEmojiID EmbedFieldPollResult = "victor_answer_emoji_id" + EmbedFieldPollResultVictorAnswerEmojiName EmbedFieldPollResult = "victor_answer_emoji_name" + EmbedFieldPollResultVictorAnswerEmojiAnimated EmbedFieldPollResult = "victor_answer_emoji_animated" +) diff --git a/discord/emoji.go b/discord/emoji.go index d91b59fe8..896efd112 100644 --- a/discord/emoji.go +++ b/discord/emoji.go @@ -15,7 +15,7 @@ type Emoji struct { GuildID snowflake.ID `json:"guild_id,omitempty"` // not present in the API but we need it Name string `json:"name,omitempty"` // may be empty for deleted emojis Roles []snowflake.ID `json:"roles,omitempty"` - Creator *User `json:"creator,omitempty"` + Creator *User `json:"user,omitempty"` RequireColons bool `json:"require_colons,omitempty"` Managed bool `json:"managed,omitempty"` Animated bool `json:"animated,omitempty"` @@ -66,9 +66,9 @@ type EmojiUpdate struct { } type PartialEmoji struct { - ID *snowflake.ID `json:"id"` - Name *string `json:"name"` - Animated bool `json:"animated"` + ID *snowflake.ID `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Animated bool `json:"animated,omitempty"` } // Reaction returns a string used for manipulating with reactions. May be empty if the Name is nil diff --git a/discord/entitlement.go b/discord/entitlement.go new file mode 100644 index 000000000..ffbd30cb1 --- /dev/null +++ b/discord/entitlement.go @@ -0,0 +1,49 @@ +package discord + +import ( + "time" + + "github.com/disgoorg/snowflake/v2" +) + +type Entitlement struct { + ID snowflake.ID `json:"id"` + SkuID snowflake.ID `json:"sku_id"` + ApplicationID snowflake.ID `json:"application_id"` + UserID *snowflake.ID `json:"user_id"` + PromotionID *snowflake.ID `json:"promotion_id"` + Type EntitlementType `json:"type"` + Deleted bool `json:"deleted"` + GiftCodeFlags int `json:"gift_code_flags"` + Consumed *bool `json:"consumed"` + StartsAt *time.Time `json:"starts_at"` + EndsAt *time.Time `json:"ends_at"` + GuildID *snowflake.ID `json:"guild_id"` + SubscriptionID *snowflake.ID `json:"subscription_id"` +} + +type EntitlementType int + +const ( + EntitlementTypePurchase EntitlementType = iota + 1 + EntitlementTypePremiumSubscription + EntitlementTypeDeveloperGift + EntitlementTypeTestModePurchase + EntitlementTypeFreePurchase + EntitlementTypeUserGift + EntitlementTypePremiumPurchase + EntitlementTypeApplicationSubscription +) + +type TestEntitlementCreate struct { + SkuID snowflake.ID `json:"sku_id"` + OwnerID snowflake.ID `json:"owner_id"` + OwnerType EntitlementOwnerType `json:"owner_type"` +} + +type EntitlementOwnerType int + +const ( + EntitlementOwnerTypeGuild EntitlementOwnerType = iota + 1 + EntitlementOwnerTypeUser +) diff --git a/discord/guild.go b/discord/guild.go index 820e5f2be..205f09bed 100644 --- a/discord/guild.go +++ b/discord/guild.go @@ -111,6 +111,7 @@ const ( GuildFeatureInvitesDisabled GuildFeature = "INVITES_DISABLED" GuildFeatureInviteSplash GuildFeature = "INVITE_SPLASH" GuildFeatureMemberVerificationGateEnabled GuildFeature = "MEMBER_VERIFICATION_GATE_ENABLED" + GuildFeatureMoreSoundboard GuildFeature = "MORE_SOUNDBOARD" GuildFeatureMoreStickers GuildFeature = "MORE_STICKERS" GuildFeatureNews GuildFeature = "NEWS" GuildFeaturePartnered GuildFeature = "PARTNERED" @@ -119,6 +120,7 @@ const ( GuildFeatureRoleIcons GuildFeature = "ROLE_ICONS" GuildFeatureRoleSubscriptionsAvailableForPurchase GuildFeature = "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" GuildFeatureRoleSubscriptionsEnabled GuildFeature = "ROLE_SUBSCRIPTIONS_ENABLED" + GuildFeatureSoundboard GuildFeature = "SOUNDBOARD" GuildFeatureTicketedEventsEnabled GuildFeature = "TICKETED_EVENTS_ENABLED" GuildFeatureVanityURL GuildFeature = "VANITY_URL" GuildFeatureVerified GuildFeature = "VERIFIED" @@ -225,6 +227,7 @@ type GatewayGuild struct { Presences []Presence `json:"presences"` StageInstances []StageInstance `json:"stage_instances"` GuildScheduledEvents []GuildScheduledEvent `json:"guild_scheduled_events"` + SoundboardSounds []SoundboardSound `json:"soundboard_sounds"` } func (g *GatewayGuild) UnmarshalJSON(data []byte) error { @@ -257,6 +260,7 @@ type OAuth2Guild struct { ID snowflake.ID `json:"id"` Name string `json:"name"` Icon *string `json:"icon"` + Banner *string `json:"banner"` Owner bool `json:"owner"` Permissions Permissions `json:"permissions"` Features []GuildFeature `json:"features"` @@ -264,6 +268,22 @@ type OAuth2Guild struct { ApproximatePresenceCount int `json:"approximate_presence_count"` } +func (g OAuth2Guild) IconURL(opts ...CDNOpt) *string { + if g.Icon == nil { + return nil + } + url := formatAssetURL(GuildIcon, opts, g.ID, *g.Icon) + return &url +} + +func (g OAuth2Guild) BannerURL(opts ...CDNOpt) *string { + if g.Banner == nil { + return nil + } + url := formatAssetURL(GuildBanner, opts, g.ID, *g.Banner) + return &url +} + // GuildWelcomeScreen is the Welcome Screen of a Guild type GuildWelcomeScreen struct { Description *string `json:"description,omitempty"` @@ -380,3 +400,8 @@ type GuildPrune struct { type GuildPruneResult struct { Pruned *int `json:"pruned"` } + +type GuildActiveThreads struct { + Threads []GuildThread `json:"threads"` + Members []ThreadMember `json:"members"` +} diff --git a/discord/guild_onboarding.go b/discord/guild_onboarding.go index 4b397673f..a3dbd17de 100644 --- a/discord/guild_onboarding.go +++ b/discord/guild_onboarding.go @@ -1,6 +1,9 @@ package discord -import "github.com/disgoorg/snowflake/v2" +import ( + "github.com/disgoorg/json" + "github.com/disgoorg/snowflake/v2" +) type GuildOnboarding struct { GuildID snowflake.ID `json:"guild_id"` @@ -21,12 +24,28 @@ type GuildOnboardingPrompt struct { } type GuildOnboardingPromptOption struct { - ID snowflake.ID `json:"id"` - ChannelIDs []snowflake.ID `json:"channel_ids"` - RoleIDs []snowflake.ID `json:"role_ids"` - Emoji PartialEmoji `json:"emoji"` - Title string `json:"title"` - Description *string `json:"description"` + ID snowflake.ID `json:"id"` + ChannelIDs []snowflake.ID `json:"channel_ids"` + RoleIDs []snowflake.ID `json:"role_ids"` + // When creating or updating prompts and their options, this field will be broken down into 3 separate fields in the payload: https://github.com/discord/discord-api-docs/pull/6479 + Emoji PartialEmoji `json:"emoji"` + Title string `json:"title"` + Description *string `json:"description"` +} + +func (o GuildOnboardingPromptOption) MarshalJSON() ([]byte, error) { + type onboardingPromptOption GuildOnboardingPromptOption + return json.Marshal(struct { + EmojiID *snowflake.ID `json:"emoji_id,omitempty"` + EmojiName *string `json:"emoji_name,omitempty"` + EmojiAnimated bool `json:"emoji_animated"` + onboardingPromptOption + }{ + EmojiID: o.Emoji.ID, + EmojiName: o.Emoji.Name, + EmojiAnimated: o.Emoji.Animated, + onboardingPromptOption: onboardingPromptOption(o), + }) } type GuildOnboardingPromptType int diff --git a/discord/guild_scheduled_event.go b/discord/guild_scheduled_event.go index 78f2bcbfc..dbd673e9d 100644 --- a/discord/guild_scheduled_event.go +++ b/discord/guild_scheduled_event.go @@ -3,54 +3,69 @@ package discord import ( "time" + "github.com/disgoorg/json" "github.com/disgoorg/snowflake/v2" ) // GuildScheduledEvent a representation of a scheduled event in a Guild (https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object) type GuildScheduledEvent struct { - ID snowflake.ID `json:"id"` - GuildID snowflake.ID `json:"guild_id"` - ChannelID *snowflake.ID `json:"channel_id"` - CreatorID snowflake.ID `json:"creator_id"` - Name string `json:"name"` - Description string `json:"description"` - ScheduledStartTime time.Time `json:"scheduled_start_time"` - ScheduledEndTime *time.Time `json:"scheduled_end_time"` - PrivacyLevel ScheduledEventPrivacyLevel `json:"privacy_level"` - Status ScheduledEventStatus `json:"status"` - EntityType ScheduledEventEntityType `json:"entity_type"` - EntityID *snowflake.ID `json:"entity_id"` - EntityMetaData *EntityMetaData `json:"entity_metadata"` - Creator User `json:"creator"` - UserCount int `json:"user_count"` + ID snowflake.ID `json:"id"` + GuildID snowflake.ID `json:"guild_id"` + ChannelID *snowflake.ID `json:"channel_id"` + CreatorID snowflake.ID `json:"creator_id"` + Name string `json:"name"` + Description string `json:"description"` + ScheduledStartTime time.Time `json:"scheduled_start_time"` + ScheduledEndTime *time.Time `json:"scheduled_end_time"` + PrivacyLevel ScheduledEventPrivacyLevel `json:"privacy_level"` + Status ScheduledEventStatus `json:"status"` + EntityType ScheduledEventEntityType `json:"entity_type"` + EntityID *snowflake.ID `json:"entity_id"` + EntityMetaData *EntityMetaData `json:"entity_metadata"` + Creator User `json:"creator"` + UserCount int `json:"user_count"` + Image *string `json:"image"` + RecurrenceRule *ScheduledEventRecurrenceRule `json:"recurrence_rule"` } func (e GuildScheduledEvent) CreatedAt() time.Time { return e.ID.Time() } +// CoverURL returns the cover URL if set or nil +func (e GuildScheduledEvent) CoverURL(opts ...CDNOpt) *string { + if e.Image == nil { + return nil + } + url := formatAssetURL(GuildScheduledEventCover, opts, e.ID, e.Image) + return &url +} + type GuildScheduledEventCreate struct { - ChannelID snowflake.ID `json:"channel_id,omitempty"` - EntityMetaData *EntityMetaData `json:"entity_metadata,omitempty"` - Name string `json:"name"` - PrivacyLevel ScheduledEventPrivacyLevel `json:"privacy_level"` - ScheduledStartTime time.Time `json:"scheduled_start_time"` - ScheduledEndTime *time.Time `json:"scheduled_end_time,omitempty"` - Description string `json:"description,omitempty"` - EntityType ScheduledEventEntityType `json:"entity_type"` - Image *Icon `json:"image,omitempty"` + ChannelID snowflake.ID `json:"channel_id,omitempty"` + EntityMetaData *EntityMetaData `json:"entity_metadata,omitempty"` + Name string `json:"name"` + PrivacyLevel ScheduledEventPrivacyLevel `json:"privacy_level"` + ScheduledStartTime time.Time `json:"scheduled_start_time"` + ScheduledEndTime *time.Time `json:"scheduled_end_time,omitempty"` + Description string `json:"description,omitempty"` + EntityType ScheduledEventEntityType `json:"entity_type"` + Image *Icon `json:"image,omitempty"` + RecurrenceRule *ScheduledEventRecurrenceRule `json:"recurrence_rule,omitempty"` } type GuildScheduledEventUpdate struct { - ChannelID *snowflake.ID `json:"channel_id,omitempty"` - EntityMetaData *EntityMetaData `json:"entity_metadata,omitempty"` - Name string `json:"name,omitempty"` - PrivacyLevel *ScheduledEventPrivacyLevel `json:"privacy_level,omitempty"` - ScheduledStartTime *time.Time `json:"scheduled_start_time,omitempty"` - ScheduledEndTime *time.Time `json:"scheduled_end_time,omitempty"` - Description *string `json:"description,omitempty"` - EntityType *ScheduledEventEntityType `json:"entity_type,omitempty"` - Status *ScheduledEventStatus `json:"status,omitempty"` + ChannelID *snowflake.ID `json:"channel_id,omitempty"` + EntityMetaData *EntityMetaData `json:"entity_metadata,omitempty"` + Name string `json:"name,omitempty"` + PrivacyLevel *ScheduledEventPrivacyLevel `json:"privacy_level,omitempty"` + ScheduledStartTime *time.Time `json:"scheduled_start_time,omitempty"` + ScheduledEndTime *time.Time `json:"scheduled_end_time,omitempty"` + Description *string `json:"description,omitempty"` + EntityType *ScheduledEventEntityType `json:"entity_type,omitempty"` + Status *ScheduledEventStatus `json:"status,omitempty"` + Image *json.Nullable[Icon] `json:"image,omitempty"` + RecurrenceRule *json.Nullable[ScheduledEventRecurrenceRule] `json:"recurrence_rule,omitempty"` } type GuildScheduledEventUser struct { @@ -86,6 +101,62 @@ const ( ScheduledEventEntityTypeExternal ) +type ScheduledEventRecurrenceRule struct { + Start time.Time `json:"start"` + End *time.Time `json:"end"` + Frequency ScheduledEventRecurrenceRuleFrequency `json:"frequency"` + Interval int `json:"interval"` + ByWeekday []ScheduledEventRecurrenceRuleWeekday `json:"by_weekday"` + ByNWeekday []ScheduledEventRecurrenceRuleNWeekday `json:"by_n_weekday"` + ByMonth []ScheduledEventRecurrenceRuleMonth `json:"by_month"` + ByMonthDay []int `json:"by_month_day"` + ByYearDay []int `json:"by_year_day"` + Count *int `json:"count"` +} + +type ScheduledEventRecurrenceRuleFrequency int + +const ( + ScheduledEventRecurrenceRuleFrequencyYearly ScheduledEventRecurrenceRuleFrequency = iota + ScheduledEventRecurrenceRuleFrequencyMonthly + ScheduledEventRecurrenceRuleFrequencyWeekly + ScheduledEventRecurrenceRuleFrequencyDaily +) + +type ScheduledEventRecurrenceRuleWeekday int + +const ( + ScheduledEventRecurrenceRuleWeekdayMonday ScheduledEventRecurrenceRuleWeekday = iota + ScheduledEventRecurrenceRuleWeekdayTuesday + ScheduledEventRecurrenceRuleWeekdayWednesday + ScheduledEventRecurrenceRuleWeekdayThursday + ScheduledEventRecurrenceRuleWeekdayFriday + ScheduledEventRecurrenceRuleWeekdaySaturday + ScheduledEventRecurrenceRuleWeekdaySunday +) + +type ScheduledEventRecurrenceRuleNWeekday struct { + N int `json:"n"` + Day ScheduledEventRecurrenceRuleWeekday `json:"day"` +} + +type ScheduledEventRecurrenceRuleMonth int + +const ( + ScheduledEventRecurrenceRuleMonthJanuary ScheduledEventRecurrenceRuleMonth = iota + 1 + ScheduledEventRecurrenceRuleMonthFebruary + ScheduledEventRecurrenceRuleMonthMarch + ScheduledEventRecurrenceRuleMonthApril + ScheduledEventRecurrenceRuleMonthMay + ScheduledEventRecurrenceRuleMonthJune + ScheduledEventRecurrenceRuleMonthJuly + ScheduledEventRecurrenceRuleMonthAugust + ScheduledEventRecurrenceRuleMonthSeptember + ScheduledEventRecurrenceRuleMonthOctober + ScheduledEventRecurrenceRuleMonthNovember + ScheduledEventRecurrenceRuleMonthDecember +) + // EntityMetaData additional metadata for the scheduled event (https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-metadata) type EntityMetaData struct { Location string `json:"location"` diff --git a/discord/icon.go b/discord/icon.go index 0b7a471d9..5ac02a3ba 100644 --- a/discord/icon.go +++ b/discord/icon.go @@ -18,11 +18,11 @@ const ( IconTypeUnknown = IconTypeJPEG ) -func (t IconType) GetMIME() string { +func (t IconType) MIME() string { return string(t) } -func (t IconType) GetHeader() string { +func (t IconType) Header() string { return "data:" + string(t) + ";base64" } @@ -56,5 +56,5 @@ func (i Icon) String() string { if len(i.Data) == 0 { return "" } - return i.Type.GetHeader() + "," + string(i.Data) + return i.Type.Header() + "," + string(i.Data) } diff --git a/discord/interaction.go b/discord/interaction.go index 3c86fc61b..74d4dd372 100644 --- a/discord/interaction.go +++ b/discord/interaction.go @@ -20,21 +20,33 @@ const ( InteractionTypeModalSubmit ) +type InteractionContextType int + +const ( + InteractionContextTypeGuild InteractionContextType = iota + InteractionContextTypeBotDM + InteractionContextTypePrivateChannel +) + type rawInteraction struct { - ID snowflake.ID `json:"id"` - Type InteractionType `json:"type"` - ApplicationID snowflake.ID `json:"application_id"` - Token string `json:"token"` - Version int `json:"version"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` + ID snowflake.ID `json:"id"` + Type InteractionType `json:"type"` + ApplicationID snowflake.ID `json:"application_id"` + Token string `json:"token"` + Version int `json:"version"` + Guild *InteractionGuild `json:"guild,omitempty"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` // Deprecated: Use Channel instead - ChannelID snowflake.ID `json:"channel_id,omitempty"` - Channel InteractionChannel `json:"channel,omitempty"` - Locale Locale `json:"locale,omitempty"` - GuildLocale *Locale `json:"guild_locale,omitempty"` - Member *ResolvedMember `json:"member,omitempty"` - User *User `json:"user,omitempty"` - AppPermissions *Permissions `json:"app_permissions,omitempty"` + ChannelID snowflake.ID `json:"channel_id,omitempty"` + Channel InteractionChannel `json:"channel,omitempty"` + Locale Locale `json:"locale,omitempty"` + GuildLocale *Locale `json:"guild_locale,omitempty"` + Member *ResolvedMember `json:"member,omitempty"` + User *User `json:"user,omitempty"` + AppPermissions *Permissions `json:"app_permissions,omitempty"` + Entitlements []Entitlement `json:"entitlements"` + AuthorizingIntegrationOwners map[ApplicationIntegrationType]snowflake.ID `json:"authorizing_integration_owners"` + Context InteractionContextType `json:"context"` } // Interaction is used for easier unmarshalling of different Interaction(s) @@ -44,6 +56,7 @@ type Interaction interface { ApplicationID() snowflake.ID Token() string Version() int + PartialGuild() *InteractionGuild GuildID() *snowflake.ID // Deprecated: Use Interaction.Channel instead ChannelID() snowflake.ID @@ -53,6 +66,9 @@ type Interaction interface { Member() *ResolvedMember User() User AppPermissions() *Permissions + Entitlements() []Entitlement + AuthorizingIntegrationOwners() map[ApplicationIntegrationType]snowflake.ID + Context() InteractionContextType CreatedAt() time.Time interaction() @@ -108,6 +124,30 @@ func UnmarshalInteraction(data []byte) (Interaction, error) { return interaction, nil } +type ResolvedData struct { + Users map[snowflake.ID]User `json:"users,omitempty"` + Members map[snowflake.ID]ResolvedMember `json:"members,omitempty"` + Roles map[snowflake.ID]Role `json:"roles,omitempty"` + Channels map[snowflake.ID]ResolvedChannel `json:"channels,omitempty"` + Attachments map[snowflake.ID]Attachment `json:"attachments,omitempty"` +} + +func (r *ResolvedData) UnmarshalJSON(data []byte) error { + type resolvedData ResolvedData + var v resolvedData + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *r = ResolvedData(v) + for id, member := range r.Members { + if user, ok := r.Users[id]; ok { + member.User = user + r.Members[id] = member + } + } + return nil +} + type ResolvedMember struct { Member Permissions Permissions `json:"permissions,omitempty"` @@ -165,3 +205,9 @@ func (c InteractionChannel) MarshalJSON() ([]byte, error) { return json.Merge(mData, pData) } + +type InteractionGuild struct { + ID snowflake.ID `json:"id"` + Locale Locale `json:"locale"` + Features []GuildFeature `json:"features"` +} diff --git a/discord/interaction_application_command.go b/discord/interaction_application_command.go index 03c9f3aa2..0ea38a5f0 100644 --- a/discord/interaction_application_command.go +++ b/discord/interaction_application_command.go @@ -58,6 +58,10 @@ func (i *ApplicationCommandInteraction) UnmarshalJSON(data []byte) error { v.Resolved.Messages[id] = msg } } + case ApplicationCommandTypePrimaryEntryPoint: + v := EntryPointCommandInteractionData{} + err = json.Unmarshal(interaction.Data, &v) + interactionData = v default: return fmt.Errorf("unknown application rawInteraction data with type %d received", cType.Type) @@ -70,6 +74,7 @@ func (i *ApplicationCommandInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.applicationID = interaction.ApplicationID i.baseInteraction.token = interaction.Token i.baseInteraction.version = interaction.Version + i.baseInteraction.guild = interaction.Guild i.baseInteraction.guildID = interaction.GuildID i.baseInteraction.channelID = interaction.ChannelID i.baseInteraction.channel = interaction.Channel @@ -78,6 +83,9 @@ func (i *ApplicationCommandInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.member = interaction.Member i.baseInteraction.user = interaction.User i.baseInteraction.appPermissions = interaction.AppPermissions + i.baseInteraction.entitlements = interaction.Entitlements + i.baseInteraction.authorizingIntegrationOwners = interaction.AuthorizingIntegrationOwners + i.baseInteraction.context = interaction.Context i.Data = interactionData return nil @@ -89,19 +97,23 @@ func (i ApplicationCommandInteraction) MarshalJSON() ([]byte, error) { Data ApplicationCommandInteractionData `json:"data"` }{ rawInteraction: rawInteraction{ - ID: i.id, - Type: i.Type(), - ApplicationID: i.applicationID, - Token: i.token, - Version: i.version, - GuildID: i.guildID, - ChannelID: i.channelID, - Channel: i.channel, - Locale: i.locale, - GuildLocale: i.guildLocale, - Member: i.member, - User: i.user, - AppPermissions: i.appPermissions, + ID: i.id, + Type: i.Type(), + ApplicationID: i.applicationID, + Token: i.token, + Version: i.version, + Guild: i.guild, + GuildID: i.guildID, + ChannelID: i.channelID, + Channel: i.channel, + Locale: i.locale, + GuildLocale: i.guildLocale, + Member: i.member, + User: i.user, + AppPermissions: i.appPermissions, + Entitlements: i.entitlements, + AuthorizingIntegrationOwners: i.authorizingIntegrationOwners, + Context: i.context, }, Data: i.Data, }) @@ -123,6 +135,10 @@ func (i ApplicationCommandInteraction) MessageCommandInteractionData() MessageCo return i.Data.(MessageCommandInteractionData) } +func (i ApplicationCommandInteraction) EntryPointCommandInteractionData() EntryPointCommandInteractionData { + return i.Data.(EntryPointCommandInteractionData) +} + func (ApplicationCommandInteraction) interaction() {} type ApplicationCommandInteractionData interface { @@ -139,7 +155,7 @@ type rawSlashCommandInteractionData struct { Name string `json:"name"` Type ApplicationCommandType `json:"type"` GuildID *snowflake.ID `json:"guild_id,omitempty"` - Resolved SlashCommandResolved `json:"resolved"` + Resolved ResolvedData `json:"resolved"` Options []internalSlashCommandOption `json:"options"` } @@ -170,7 +186,7 @@ type SlashCommandInteractionData struct { guildID *snowflake.ID SubCommandName *string SubCommandGroupName *string - Resolved SlashCommandResolved + Resolved ResolvedData Options map[string]SlashCommandOption } @@ -498,30 +514,6 @@ func (d SlashCommandInteractionData) FindAll(optionFindFunc func(option SlashCom func (SlashCommandInteractionData) applicationCommandInteractionData() {} -type SlashCommandResolved struct { - Users map[snowflake.ID]User `json:"users,omitempty"` - Members map[snowflake.ID]ResolvedMember `json:"members,omitempty"` - Roles map[snowflake.ID]Role `json:"roles,omitempty"` - Channels map[snowflake.ID]ResolvedChannel `json:"channels,omitempty"` - Attachments map[snowflake.ID]Attachment `json:"attachments,omitempty"` -} - -func (r *SlashCommandResolved) UnmarshalJSON(data []byte) error { - type slashCommandResolved SlashCommandResolved - var v slashCommandResolved - if err := json.Unmarshal(data, &v); err != nil { - return err - } - *r = SlashCommandResolved(v) - for id, member := range r.Members { - if user, ok := r.Users[id]; ok { - member.User = user - r.Members[id] = member - } - } - return nil -} - type ContextCommandInteractionData interface { ApplicationCommandInteractionData TargetID() snowflake.ID @@ -703,3 +695,54 @@ func (MessageCommandInteractionData) contextCommandInteractionData() {} type MessageCommandResolved struct { Messages map[snowflake.ID]Message `json:"messages,omitempty"` } + +var ( + _ ApplicationCommandInteractionData = (*EntryPointCommandInteractionData)(nil) +) + +type rawEntryPointCommandInteractionData struct { + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Type ApplicationCommandType `json:"type"` +} + +type EntryPointCommandInteractionData struct { + id snowflake.ID + name string +} + +func (d *EntryPointCommandInteractionData) UnmarshalJSON(data []byte) error { + var iData rawEntryPointCommandInteractionData + if err := json.Unmarshal(data, &iData); err != nil { + return err + } + d.id = iData.ID + d.name = iData.Name + return nil +} + +func (d *EntryPointCommandInteractionData) MarshalJSON() ([]byte, error) { + return json.Marshal(rawEntryPointCommandInteractionData{ + ID: d.id, + Name: d.name, + Type: d.Type(), + }) +} + +func (EntryPointCommandInteractionData) Type() ApplicationCommandType { + return ApplicationCommandTypePrimaryEntryPoint +} + +func (d EntryPointCommandInteractionData) CommandID() snowflake.ID { + return d.id +} + +func (d EntryPointCommandInteractionData) CommandName() string { + return d.name +} + +func (d EntryPointCommandInteractionData) GuildID() *snowflake.ID { + return nil +} + +func (EntryPointCommandInteractionData) applicationCommandInteractionData() {} diff --git a/discord/interaction_autocomplete.go b/discord/interaction_autocomplete.go index 3d52f8729..640fcfb4c 100644 --- a/discord/interaction_autocomplete.go +++ b/discord/interaction_autocomplete.go @@ -27,6 +27,7 @@ func (i *AutocompleteInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.applicationID = interaction.ApplicationID i.baseInteraction.token = interaction.Token i.baseInteraction.version = interaction.Version + i.baseInteraction.guild = interaction.Guild i.baseInteraction.guildID = interaction.GuildID i.baseInteraction.channelID = interaction.ChannelID i.baseInteraction.channel = interaction.Channel @@ -35,6 +36,9 @@ func (i *AutocompleteInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.member = interaction.Member i.baseInteraction.user = interaction.User i.baseInteraction.appPermissions = interaction.AppPermissions + i.baseInteraction.entitlements = interaction.Entitlements + i.baseInteraction.authorizingIntegrationOwners = interaction.AuthorizingIntegrationOwners + i.baseInteraction.context = interaction.Context i.Data = interaction.Data return nil @@ -46,19 +50,23 @@ func (i AutocompleteInteraction) MarshalJSON() ([]byte, error) { Data AutocompleteInteractionData `json:"data"` }{ rawInteraction: rawInteraction{ - ID: i.id, - Type: i.Type(), - ApplicationID: i.applicationID, - Token: i.token, - Version: i.version, - GuildID: i.guildID, - ChannelID: i.channelID, - Channel: i.channel, - Locale: i.locale, - GuildLocale: i.guildLocale, - Member: i.member, - User: i.user, - AppPermissions: i.appPermissions, + ID: i.id, + Type: i.Type(), + ApplicationID: i.applicationID, + Token: i.token, + Version: i.version, + Guild: i.guild, + GuildID: i.guildID, + ChannelID: i.channelID, + Channel: i.channel, + Locale: i.locale, + GuildLocale: i.guildLocale, + Member: i.member, + User: i.user, + AppPermissions: i.appPermissions, + Entitlements: i.entitlements, + AuthorizingIntegrationOwners: i.authorizingIntegrationOwners, + Context: i.context, }, Data: i.Data, }) @@ -195,6 +203,13 @@ func (d AutocompleteInteractionData) CommandPath() string { return path } +func (d AutocompleteInteractionData) Focused() AutocompleteOption { + option, _ := d.Find(func(option AutocompleteOption) bool { + return option.Focused + }) + return option +} + func (d AutocompleteInteractionData) Option(name string) (AutocompleteOption, bool) { option, ok := d.Options[name] return option, ok diff --git a/discord/interaction_base.go b/discord/interaction_base.go index 7f5730ab9..f72c0bb4d 100644 --- a/discord/interaction_base.go +++ b/discord/interaction_base.go @@ -7,18 +7,22 @@ import ( ) type baseInteraction struct { - id snowflake.ID - applicationID snowflake.ID - token string - version int - guildID *snowflake.ID - channelID snowflake.ID - channel InteractionChannel - locale Locale - guildLocale *Locale - member *ResolvedMember - user *User - appPermissions *Permissions + id snowflake.ID + applicationID snowflake.ID + token string + version int + guild *InteractionGuild + guildID *snowflake.ID + channelID snowflake.ID + channel InteractionChannel + locale Locale + guildLocale *Locale + member *ResolvedMember + user *User + appPermissions *Permissions + entitlements []Entitlement + authorizingIntegrationOwners map[ApplicationIntegrationType]snowflake.ID + context InteractionContextType } func (i baseInteraction) ID() snowflake.ID { @@ -33,6 +37,9 @@ func (i baseInteraction) Token() string { func (i baseInteraction) Version() int { return i.version } +func (i baseInteraction) PartialGuild() *InteractionGuild { + return i.guild +} func (i baseInteraction) GuildID() *snowflake.ID { return i.guildID } @@ -64,6 +71,18 @@ func (i baseInteraction) AppPermissions() *Permissions { return i.appPermissions } +func (i baseInteraction) Entitlements() []Entitlement { + return i.entitlements +} + +func (i baseInteraction) AuthorizingIntegrationOwners() map[ApplicationIntegrationType]snowflake.ID { + return i.authorizingIntegrationOwners +} + +func (i baseInteraction) Context() InteractionContextType { + return i.context +} + func (i baseInteraction) CreatedAt() time.Time { return i.id.Time() } diff --git a/discord/interaction_callback.go b/discord/interaction_callback.go new file mode 100644 index 000000000..cab381baa --- /dev/null +++ b/discord/interaction_callback.go @@ -0,0 +1,27 @@ +package discord + +import "github.com/disgoorg/snowflake/v2" + +type InteractionCallbackResponse struct { + Interaction InteractionCallback `json:"interaction"` + Resource *InteractionCallbackResource `json:"resource"` +} + +type InteractionCallback struct { + ID snowflake.ID `json:"id"` + Type InteractionType `json:"type"` + ActivityInstanceID string `json:"activity_instance_id"` + ResponseMessageID snowflake.ID `json:"response_message_id"` + ResponseMessageLoading bool `json:"response_message_loading"` + ResponseMessageEphemeral bool `json:"response_message_ephemeral"` +} + +type InteractionCallbackResource struct { + Type InteractionResponseType `json:"type"` + ActivityInstance *InteractionCallbackActivityInstance `json:"activity_instance"` + Message *Message `json:"message"` +} + +type InteractionCallbackActivityInstance struct { + ID string `json:"id"` +} diff --git a/discord/interaction_component.go b/discord/interaction_component.go index 12164e83b..8ab312237 100644 --- a/discord/interaction_component.go +++ b/discord/interaction_component.go @@ -80,6 +80,7 @@ func (i *ComponentInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.applicationID = interaction.ApplicationID i.baseInteraction.token = interaction.Token i.baseInteraction.version = interaction.Version + i.baseInteraction.guild = interaction.Guild i.baseInteraction.guildID = interaction.GuildID i.baseInteraction.channelID = interaction.ChannelID i.baseInteraction.channel = interaction.Channel @@ -88,6 +89,9 @@ func (i *ComponentInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.member = interaction.Member i.baseInteraction.user = interaction.User i.baseInteraction.appPermissions = interaction.AppPermissions + i.baseInteraction.entitlements = interaction.Entitlements + i.baseInteraction.authorizingIntegrationOwners = interaction.AuthorizingIntegrationOwners + i.baseInteraction.context = interaction.Context i.Data = interactionData i.Message = interaction.Message @@ -102,19 +106,23 @@ func (i ComponentInteraction) MarshalJSON() ([]byte, error) { Message Message `json:"message"` }{ rawInteraction: rawInteraction{ - ID: i.id, - Type: i.Type(), - ApplicationID: i.applicationID, - Token: i.token, - Version: i.version, - GuildID: i.guildID, - ChannelID: i.channelID, - Channel: i.channel, - Locale: i.locale, - GuildLocale: i.guildLocale, - Member: i.member, - User: i.user, - AppPermissions: i.appPermissions, + ID: i.id, + Type: i.Type(), + ApplicationID: i.applicationID, + Token: i.token, + Version: i.version, + Guild: i.guild, + GuildID: i.guildID, + ChannelID: i.channelID, + Channel: i.channel, + Locale: i.locale, + GuildLocale: i.guildLocale, + Member: i.member, + User: i.user, + AppPermissions: i.appPermissions, + Entitlements: i.entitlements, + AuthorizingIntegrationOwners: i.authorizingIntegrationOwners, + Context: i.context, }, Data: i.Data, Message: i.Message, diff --git a/discord/interaction_modal_submit.go b/discord/interaction_modal_submit.go index fe96e274a..204fdb458 100644 --- a/discord/interaction_modal_submit.go +++ b/discord/interaction_modal_submit.go @@ -24,6 +24,7 @@ func (i *ModalSubmitInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.applicationID = interaction.ApplicationID i.baseInteraction.token = interaction.Token i.baseInteraction.version = interaction.Version + i.baseInteraction.guild = interaction.Guild i.baseInteraction.guildID = interaction.GuildID i.baseInteraction.channelID = interaction.ChannelID i.baseInteraction.channel = interaction.Channel @@ -32,6 +33,9 @@ func (i *ModalSubmitInteraction) UnmarshalJSON(data []byte) error { i.baseInteraction.member = interaction.Member i.baseInteraction.user = interaction.User i.baseInteraction.appPermissions = interaction.AppPermissions + i.baseInteraction.entitlements = interaction.Entitlements + i.baseInteraction.authorizingIntegrationOwners = interaction.AuthorizingIntegrationOwners + i.baseInteraction.context = interaction.Context i.Data = interaction.Data return nil @@ -43,19 +47,23 @@ func (i ModalSubmitInteraction) MarshalJSON() ([]byte, error) { Data ModalSubmitInteractionData `json:"data"` }{ rawInteraction: rawInteraction{ - ID: i.id, - Type: i.Type(), - ApplicationID: i.applicationID, - Token: i.token, - Version: i.version, - GuildID: i.guildID, - ChannelID: i.channelID, - Channel: i.channel, - Locale: i.locale, - GuildLocale: i.guildLocale, - Member: i.member, - User: i.user, - AppPermissions: i.appPermissions, + ID: i.id, + Type: i.Type(), + ApplicationID: i.applicationID, + Token: i.token, + Version: i.version, + Guild: i.guild, + GuildID: i.guildID, + ChannelID: i.channelID, + Channel: i.channel, + Locale: i.locale, + GuildLocale: i.guildLocale, + Member: i.member, + User: i.user, + AppPermissions: i.appPermissions, + Entitlements: i.entitlements, + AuthorizingIntegrationOwners: i.authorizingIntegrationOwners, + Context: i.context, }, Data: i.Data, }) diff --git a/discord/interaction_ping.go b/discord/interaction_ping.go index 5d5c15717..deef7cd20 100644 --- a/discord/interaction_ping.go +++ b/discord/interaction_ping.go @@ -64,6 +64,10 @@ func (i PingInteraction) CreatedAt() time.Time { return i.id.Time() } +func (PingInteraction) PartialGuild() *InteractionGuild { + return nil +} + func (PingInteraction) GuildID() *snowflake.ID { return nil } @@ -96,4 +100,16 @@ func (PingInteraction) AppPermissions() *Permissions { return nil } +func (PingInteraction) Entitlements() []Entitlement { + return nil +} + +func (PingInteraction) AuthorizingIntegrationOwners() map[ApplicationIntegrationType]snowflake.ID { + return nil +} + +func (PingInteraction) Context() InteractionContextType { + return 0 +} + func (PingInteraction) interaction() {} diff --git a/discord/interaction_response.go b/discord/interaction_response.go index c786c16b8..b21285a5e 100644 --- a/discord/interaction_response.go +++ b/discord/interaction_response.go @@ -3,6 +3,11 @@ package discord // InteractionResponseType indicates the type of slash command response, whether it's responding immediately or deferring to edit your response later type InteractionResponseType int +// InteractionResponseTypeAcknowledge is stricly internal and will never be sent to discord. +// +// It is used to indicate that the HTTP response should be 202 Accepted +const InteractionResponseTypeAcknowledge InteractionResponseType = -1 + // Constants for the InteractionResponseType(s) const ( InteractionResponseTypePong InteractionResponseType = iota + 1 @@ -12,8 +17,11 @@ const ( InteractionResponseTypeDeferredCreateMessage InteractionResponseTypeDeferredUpdateMessage InteractionResponseTypeUpdateMessage - InteractionResponseTypeApplicationCommandAutocompleteResult + InteractionResponseTypeAutocompleteResult InteractionResponseTypeModal + InteractionResponseTypePremiumRequired + _ + InteractionResponseTypeLaunchActivity ) // InteractionResponse is how you answer interactions. If an answer is not sent within 3 seconds of receiving it, the interaction is failed, and you will be unable to respond to it. @@ -45,6 +53,8 @@ type AutocompleteResult struct { func (AutocompleteResult) interactionCallbackData() {} type AutocompleteChoice interface { + ChoiceName() string + autoCompleteChoice() } @@ -54,6 +64,10 @@ type AutocompleteChoiceString struct { Value string `json:"value"` } +func (c AutocompleteChoiceString) ChoiceName() string { + return c.Name +} + func (AutocompleteChoiceString) autoCompleteChoice() {} type AutocompleteChoiceInt struct { @@ -62,6 +76,10 @@ type AutocompleteChoiceInt struct { Value int `json:"value"` } +func (c AutocompleteChoiceInt) ChoiceName() string { + return c.Name +} + func (AutocompleteChoiceInt) autoCompleteChoice() {} type AutocompleteChoiceFloat struct { @@ -70,4 +88,8 @@ type AutocompleteChoiceFloat struct { Value float64 `json:"value"` } +func (c AutocompleteChoiceFloat) ChoiceName() string { + return c.Name +} + func (AutocompleteChoiceFloat) autoCompleteChoice() {} diff --git a/discord/invite.go b/discord/invite.go index 37c96324d..3dd151297 100644 --- a/discord/invite.go +++ b/discord/invite.go @@ -18,10 +18,10 @@ const ( // Invite is a partial invite struct type Invite struct { + Type InviteType `json:"type"` Code string `json:"code"` Guild *InviteGuild `json:"guild"` Channel *InviteChannel `json:"channel"` - ChannelID snowflake.ID `json:"channel_id"` Inviter *User `json:"inviter"` TargetUser *User `json:"target_user"` TargetType InviteTargetType `json:"target_user_type"` @@ -35,6 +35,14 @@ func (i Invite) URL() string { return InviteURL(i.Code) } +type InviteType int + +const ( + InviteTypeGuild InviteType = iota + InviteTypeGroupDM + InviteTypeFriend +) + type ExtendedInvite struct { Invite Uses int `json:"uses"` diff --git a/discord/member.go b/discord/member.go index 126661049..6338eff62 100644 --- a/discord/member.go +++ b/discord/member.go @@ -13,17 +13,19 @@ var _ Mentionable = (*Member)(nil) // Member is a discord GuildMember type Member struct { - User User `json:"user"` - Nick *string `json:"nick"` - Avatar *string `json:"avatar"` - RoleIDs []snowflake.ID `json:"roles,omitempty"` - JoinedAt time.Time `json:"joined_at"` - PremiumSince *time.Time `json:"premium_since,omitempty"` - Deaf bool `json:"deaf,omitempty"` - Mute bool `json:"mute,omitempty"` - Flags MemberFlags `json:"flags"` - Pending bool `json:"pending"` - CommunicationDisabledUntil *time.Time `json:"communication_disabled_until"` + User User `json:"user"` + Nick *string `json:"nick"` + Avatar *string `json:"avatar"` + Banner *string `json:"banner"` + RoleIDs []snowflake.ID `json:"roles,omitempty"` + JoinedAt time.Time `json:"joined_at"` + PremiumSince *time.Time `json:"premium_since,omitempty"` + Deaf bool `json:"deaf,omitempty"` + Mute bool `json:"mute,omitempty"` + Flags MemberFlags `json:"flags"` + Pending bool `json:"pending"` + CommunicationDisabledUntil *time.Time `json:"communication_disabled_until"` + AvatarDecorationData *AvatarDecorationData `json:"avatar_decoration_data"` // This field is not present everywhere in the API and often populated by disgo GuildID snowflake.ID `json:"guild_id"` @@ -52,10 +54,7 @@ func (m Member) EffectiveAvatarURL(opts ...CDNOpt) string { if m.Avatar == nil { return m.User.EffectiveAvatarURL(opts...) } - if avatar := m.AvatarURL(opts...); avatar != nil { - return *avatar - } - return "" + return formatAssetURL(MemberAvatar, opts, m.GuildID, m.User.ID, *m.Avatar) } // AvatarURL returns the guild-specific avatar URL of the user if set or nil @@ -67,6 +66,35 @@ func (m Member) AvatarURL(opts ...CDNOpt) *string { return &url } +// EffectiveBannerURL returns the guild-specific banner URL of the user if set, falling back to the banner URL of the user +func (m Member) EffectiveBannerURL(opts ...CDNOpt) string { + if m.Banner == nil { + if banner := m.User.BannerURL(opts...); banner != nil { + return *banner + } + return "" + } + return formatAssetURL(MemberBanner, opts, m.GuildID, m.User.ID, *m.Banner) +} + +// BannerURL returns the guild-specific banner URL of the user if set or nil +func (m Member) BannerURL(opts ...CDNOpt) *string { + if m.Banner == nil { + return nil + } + url := formatAssetURL(MemberBanner, opts, m.GuildID, m.User.ID, *m.Banner) + return &url +} + +// AvatarDecorationURL returns the avatar decoration URL if set or nil +func (m Member) AvatarDecorationURL(opts ...CDNOpt) *string { + if m.AvatarDecorationData == nil { + return nil + } + url := formatAssetURL(AvatarDecoration, opts, m.AvatarDecorationData.Asset) + return &url +} + func (m Member) CreatedAt() time.Time { return m.User.CreatedAt() } @@ -103,6 +131,12 @@ const ( MemberFlagCompletedOnboarding MemberFlagBypassesVerification MemberFlagStartedOnboarding + MemberFlagIsGuest + MemberFlagStartedHomeActions + MemberFlagCompletedHomeActions + MemberFlagAutomodQuarantinedUsername + _ + MemberFlagDMSettingsUpsellAcknowledged MemberFlagsNone MemberFlags = 0 ) diff --git a/discord/mentionable.go b/discord/mentionable.go index 3f8c4087e..dd6a83f0a 100644 --- a/discord/mentionable.go +++ b/discord/mentionable.go @@ -20,7 +20,8 @@ var ( MentionTypeSlashCommand = MentionType{regexp.MustCompile(``)} MentionTypeHere = MentionType{regexp.MustCompile(`@here`)} MentionTypeEveryone = MentionType{regexp.MustCompile(`@everyone`)} - MentionTypeGuildNavigation = MentionType{regexp.MustCompile("")} + MentionTypeGuildNavigation = MentionType{regexp.MustCompile("")} + MentionTypeLinkedRole = MentionType{regexp.MustCompile(``)} ) type Mentionable interface { @@ -83,3 +84,11 @@ func NavigationCustomizeMention() string { func NavigationGuideMention() string { return "" } + +func NavigationLinkedRoles() string { + return "" +} + +func NavigationLinkedRole(id snowflake.ID) string { + return fmt.Sprintf("", id) +} diff --git a/discord/message.go b/discord/message.go index ea46176b7..9c34e538d 100644 --- a/discord/message.go +++ b/discord/message.go @@ -1,7 +1,9 @@ package discord import ( + "bytes" "fmt" + "strconv" "time" "github.com/disgoorg/json" @@ -21,7 +23,7 @@ const ( MessageTypeCall MessageTypeChannelNameChange MessageTypeChannelIconChange - ChannelPinnedMessage + MessageTypeChannelPinnedMessage MessageTypeUserJoin MessageTypeGuildBoost MessageTypeGuildBoostTier1 @@ -55,6 +57,13 @@ const ( MessageTypeGuildIncidentAlertModeDisabled MessageTypeGuildIncidentReportRaid MessageTypeGuildIncidentReportFalseAlarm + _ + _ + _ + _ + MessageTypePurchaseNotification + _ + MessageTypePollResult ) func (t MessageType) System() bool { @@ -70,9 +79,7 @@ func (t MessageType) System() bool { func (t MessageType) Deleteable() bool { switch t { case MessageTypeRecipientAdd, MessageTypeRecipientRemove, MessageTypeCall, - MessageTypeChannelNameChange, MessageTypeChannelIconChange, MessageTypeGuildDiscoveryDisqualified, - MessageTypeGuildDiscoveryRequalified, MessageTypeGuildDiscoveryGracePeriodInitialWarning, - MessageTypeGuildDiscoveryGracePeriodFinalWarning, MessageTypeThreadStarterMessage: + MessageTypeChannelNameChange, MessageTypeChannelIconChange, MessageTypeThreadStarterMessage: return false default: @@ -99,7 +106,7 @@ type Message struct { Mentions []User `json:"mentions"` MentionEveryone bool `json:"mention_everyone"` MentionRoles []snowflake.ID `json:"mention_roles"` - MentionChannels []Channel `json:"mention_channels"` + MentionChannels []MentionChannel `json:"mention_channels"` Pinned bool `json:"pinned"` EditedTimestamp *time.Time `json:"edited_timestamp"` Author User `json:"author"` @@ -109,6 +116,7 @@ type Message struct { Type MessageType `json:"type"` Flags MessageFlags `json:"flags"` MessageReference *MessageReference `json:"message_reference,omitempty"` + MessageSnapshots []MessageSnapshot `json:"message_snapshots,omitempty"` Interaction *MessageInteraction `json:"interaction,omitempty"` WebhookID *snowflake.ID `json:"webhook_id,omitempty"` Activity *MessageActivity `json:"activity,omitempty"` @@ -120,6 +128,11 @@ type Message struct { Thread *MessageThread `json:"thread,omitempty"` Position *int `json:"position,omitempty"` RoleSubscriptionData *RoleSubscriptionData `json:"role_subscription_data,omitempty"` + InteractionMetadata *InteractionMetadata `json:"interaction_metadata,omitempty"` + Resolved *ResolvedData `json:"resolved,omitempty"` + Poll *Poll `json:"poll,omitempty"` + Call *MessageCall `json:"call,omitempty"` + Nonce Nonce `json:"nonce,omitempty"` } func (m *Message) UnmarshalJSON(data []byte) error { @@ -136,10 +149,7 @@ func (m *Message) UnmarshalJSON(data []byte) error { *m = Message(v.message) if len(v.Components) > 0 { - m.Components = make([]ContainerComponent, len(v.Components)) - for i := range v.Components { - m.Components[i] = v.Components[i].Component.(ContainerComponent) - } + m.Components = unmarshalComponents(v.Components) } if m.Member != nil && m.GuildID != nil { @@ -341,6 +351,13 @@ func (m Message) JumpURL() string { return fmt.Sprintf(MessageURLFmt, guildID, m.ChannelID, m.ID) // duplicate code, but there isn't a better way without sacrificing user convenience } +type MentionChannel struct { + ID snowflake.ID `json:"id"` + GuildID snowflake.ID `json:"guild_id"` + Type ChannelType `json:"type"` + Name string `json:"name"` +} + type MessageThread struct { GuildThread Member ThreadMember `json:"member"` @@ -367,6 +384,13 @@ type ReactionCountDetails struct { Normal int `json:"normal"` } +type MessageReactionType int + +const ( + MessageReactionTypeNormal MessageReactionType = iota + MessageReactionTypeBurst +) + // MessageActivityType is the type of MessageActivity https://com/developers/docs/resources/channel#message-object-message-activity-types type MessageActivityType int @@ -396,10 +420,57 @@ type MessageApplication struct { // MessageReference is a reference to another message type MessageReference struct { - MessageID *snowflake.ID `json:"message_id"` - ChannelID *snowflake.ID `json:"channel_id,omitempty"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - FailIfNotExists bool `json:"fail_if_not_exists,omitempty"` + Type MessageReferenceType `json:"type,omitempty"` + MessageID *snowflake.ID `json:"message_id"` + ChannelID *snowflake.ID `json:"channel_id,omitempty"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + FailIfNotExists bool `json:"fail_if_not_exists,omitempty"` +} + +type MessageReferenceType int + +const ( + MessageReferenceTypeDefault MessageReferenceType = iota + MessageReferenceTypeForward +) + +type MessageSnapshot struct { + Message PartialMessage `json:"message"` +} + +type PartialMessage struct { + Type MessageType `json:"type"` + Content string `json:"content,omitempty"` + Embeds []Embed `json:"embeds,omitempty"` + Attachments []Attachment `json:"attachments"` + CreatedAt time.Time `json:"timestamp"` + EditedTimestamp *time.Time `json:"edited_timestamp"` + Flags MessageFlags `json:"flags"` + Mentions []User `json:"mentions"` + MentionRoles []snowflake.ID `json:"mention_roles"` + Stickers []Sticker `json:"stickers"` + StickerItems []MessageSticker `json:"sticker_items,omitempty"` + Components []ContainerComponent `json:"components,omitempty"` +} + +func (m *PartialMessage) UnmarshalJSON(data []byte) error { + type partialMessage PartialMessage + var v struct { + Components []UnmarshalComponent `json:"components"` + partialMessage + } + + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + *m = PartialMessage(v.partialMessage) + + if len(v.Components) > 0 { + m.Components = unmarshalComponents(v.Components) + } + + return nil } // MessageInteraction is sent on the Message object when the message is a response to an interaction @@ -433,6 +504,7 @@ const ( _ MessageFlagSuppressNotifications MessageFlagIsVoiceMessage + MessageFlagHasSnapshot MessageFlagsNone MessageFlags = 0 ) @@ -462,3 +534,59 @@ type RoleSubscriptionData struct { TotalMonthsSubscribed int `json:"total_months_subscribed"` IsRenewal bool `json:"is_renewal"` } + +type InteractionMetadata struct { + ID snowflake.ID `json:"id"` + Type InteractionType `json:"type"` + User User `json:"user"` + AuthorizingIntegrationOwners map[ApplicationIntegrationType]snowflake.ID `json:"authorizing_integration_owners"` + OriginalResponseMessageID *snowflake.ID `json:"original_response_message_id"` + // This field will only be present for application command interactions of ApplicationCommandTypeUser. + // See https://discord.com/developers/docs/resources/message#message-interaction-metadata-object-application-command-interaction-metadata-structure + TargetUser *User `json:"target_user"` + // This field will only be present for application command interactions of ApplicationCommandTypeMessage. + // See https://discord.com/developers/docs/resources/message#message-interaction-metadata-object-application-command-interaction-metadata-structure + TargetMessageID *snowflake.ID `json:"target_message_id"` + // This field will only be present for InteractionTypeComponent interactions. + // See https://discord.com/developers/docs/resources/message#message-interaction-metadata-object-message-component-interaction-metadata-structure + InteractedMessageID *snowflake.ID `json:"interacted_message_id"` + // This field will only be present for InteractionTypeModalSubmit interactions. + // See https://discord.com/developers/docs/resources/message#message-interaction-metadata-object-modal-submit-interaction-metadata-structure + TriggeringInteractionMetadata *InteractionMetadata `json:"triggering_interaction_metadata"` +} + +type MessageCall struct { + Participants []snowflake.ID `json:"participants"` + EndedTimestamp *time.Time `json:"ended_timestamp"` +} + +func unmarshalComponents(components []UnmarshalComponent) []ContainerComponent { + containerComponents := make([]ContainerComponent, len(components)) + for i := range components { + containerComponents[i] = components[i].Component.(ContainerComponent) + } + return containerComponents +} + +// Nonce is a string or int used when sending a message to discord. +type Nonce string + +// UnmarshalJSON unmarshals the Nonce from a string or int. +func (n *Nonce) UnmarshalJSON(b []byte) error { + if bytes.Equal(b, []byte("null")) { + return nil + } + + unquoted, err := strconv.Unquote(string(b)) + if err != nil { + i, err := strconv.ParseInt(string(b), 10, 64) + if err != nil { + return err + } + *n = Nonce(strconv.FormatInt(i, 10)) + } else { + *n = Nonce(unquoted) + } + + return nil +} diff --git a/discord/message_create.go b/discord/message_create.go index 776a7d00e..e21cc4cba 100644 --- a/discord/message_create.go +++ b/discord/message_create.go @@ -17,6 +17,8 @@ type MessageCreate struct { AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` MessageReference *MessageReference `json:"message_reference,omitempty"` Flags MessageFlags `json:"flags,omitempty"` + EnforceNonce bool `json:"enforce_nonce,omitempty"` + Poll *PollCreate `json:"poll,omitempty"` } func (MessageCreate) interactionCallbackData() {} diff --git a/discord/message_create_builder.go b/discord/message_create_builder.go index bdf04fae7..cf1178c17 100644 --- a/discord/message_create_builder.go +++ b/discord/message_create_builder.go @@ -32,6 +32,18 @@ func (b *MessageCreateBuilder) SetContentf(content string, a ...any) *MessageCre return b.SetContent(fmt.Sprintf(content, a...)) } +// SetNonce sets the Message nonce +func (b *MessageCreateBuilder) SetNonce(nonce string) *MessageCreateBuilder { + b.Nonce = nonce + return b +} + +// SetEnforceNonce sets whether the Message should be checked for uniqueness (use with SetNonce) +func (b *MessageCreateBuilder) SetEnforceNonce(enforce bool) *MessageCreateBuilder { + b.EnforceNonce = enforce + return b +} + // SetTTS sets whether the Message should be text to speech func (b *MessageCreateBuilder) SetTTS(tts bool) *MessageCreateBuilder { b.TTS = tts @@ -239,6 +251,18 @@ func (b *MessageCreateBuilder) SetSuppressEmbeds(suppressEmbeds bool) *MessageCr return b } +// SetPoll sets the Poll of the Message +func (b *MessageCreateBuilder) SetPoll(poll PollCreate) *MessageCreateBuilder { + b.Poll = &poll + return b +} + +// ClearPoll clears the Poll of the Message +func (b *MessageCreateBuilder) ClearPoll() *MessageCreateBuilder { + b.Poll = nil + return b +} + // Build builds the MessageCreateBuilder to a MessageCreate struct func (b *MessageCreateBuilder) Build() MessageCreate { return b.MessageCreate diff --git a/discord/message_update.go b/discord/message_update.go index d04e6dcc3..ddcd48a09 100644 --- a/discord/message_update.go +++ b/discord/message_update.go @@ -8,7 +8,10 @@ type MessageUpdate struct { Attachments *[]AttachmentUpdate `json:"attachments,omitempty"` Files []*File `json:"-"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` - Flags *MessageFlags `json:"flags,omitempty"` + // Flags are the MessageFlags of the message. + // Be careful not to override the current flags when editing messages from other users - this will result in a permission error. + // Use MessageFlags.Add for flags like discord.MessageFlagSuppressEmbeds. + Flags *MessageFlags `json:"flags,omitempty"` } func (MessageUpdate) interactionCallbackData() {} diff --git a/discord/message_update_builder.go b/discord/message_update_builder.go index e7d0c8bf4..c81ca37c5 100644 --- a/discord/message_update_builder.go +++ b/discord/message_update_builder.go @@ -177,7 +177,7 @@ func (b *MessageUpdateBuilder) RemoveFile(i int) *MessageUpdateBuilder { // RetainAttachments removes all Attachment(s) from this Message except the ones provided func (b *MessageUpdateBuilder) RetainAttachments(attachments ...Attachment) *MessageUpdateBuilder { if b.Attachments == nil { - b.Attachments = new([]AttachmentUpdate) + b.Attachments = &[]AttachmentUpdate{} } for _, attachment := range attachments { *b.Attachments = append(*b.Attachments, AttachmentKeep{ID: attachment.ID}) @@ -188,7 +188,7 @@ func (b *MessageUpdateBuilder) RetainAttachments(attachments ...Attachment) *Mes // RetainAttachmentsByID removes all Attachment(s) from this Message except the ones provided func (b *MessageUpdateBuilder) RetainAttachmentsByID(attachmentIDs ...snowflake.ID) *MessageUpdateBuilder { if b.Attachments == nil { - b.Attachments = new([]AttachmentUpdate) + b.Attachments = &[]AttachmentUpdate{} } for _, attachmentID := range attachmentIDs { *b.Attachments = append(*b.Attachments, AttachmentKeep{ID: attachmentID}) @@ -207,7 +207,9 @@ func (b *MessageUpdateBuilder) ClearAllowedMentions() *MessageUpdateBuilder { return b.SetAllowedMentions(nil) } -// SetFlags sets the message flags of the Message +// SetFlags sets the MessageFlags of the Message. +// Be careful not to override the current flags when editing messages from other users - this will result in a permission error. +// Use SetSuppressEmbeds or AddFlags for flags like discord.MessageFlagSuppressEmbeds. func (b *MessageUpdateBuilder) SetFlags(flags MessageFlags) *MessageUpdateBuilder { if b.Flags == nil { b.Flags = new(MessageFlags) diff --git a/discord/permissions.go b/discord/permissions.go index fe5a826a6..2206f295f 100644 --- a/discord/permissions.go +++ b/discord/permissions.go @@ -59,10 +59,14 @@ const ( PermissionModerateMembers PermissionViewCreatorMonetizationAnalytics PermissionUseSoundboard - _ - _ + PermissionCreateGuildExpressions + PermissionCreateEvents PermissionUseExternalSounds PermissionSendVoiceMessages + _ + _ + PermissionSendPolls + PermissionUseExternalApps PermissionsAllText = PermissionViewChannel | PermissionSendMessages | @@ -72,7 +76,9 @@ const ( PermissionAttachFiles | PermissionReadMessageHistory | PermissionMentionEveryone | - PermissionSendVoiceMessages + PermissionSendVoiceMessages | + PermissionSendPolls | + PermissionUseExternalApps PermissionsAllThread = PermissionManageThreads | PermissionCreatePublicThreads | @@ -91,7 +97,10 @@ const ( PermissionUseSoundboard | PermissionUseExternalSounds | PermissionRequestToSpeak | - PermissionUseEmbeddedActivities + PermissionUseEmbeddedActivities | + PermissionCreateGuildExpressions | + PermissionCreateEvents | + PermissionManageEvents PermissionsAllChannel = PermissionsAllText | PermissionsAllThread | @@ -116,7 +125,6 @@ const ( PermissionManageRoles | PermissionChangeNickname | PermissionManageNicknames | - PermissionManageEvents | PermissionModerateMembers PermissionsNone Permissions = 0 @@ -168,6 +176,8 @@ var permissions = map[Permissions]string{ PermissionStream: "Video", PermissionViewGuildInsights: "View Server Insights", PermissionSendVoiceMessages: "Send Voice Messages", + PermissionSendPolls: "Create Polls", + PermissionUseExternalApps: "Use External Apps", } func (p Permissions) String() string { diff --git a/discord/poll.go b/discord/poll.go new file mode 100644 index 000000000..d54d95475 --- /dev/null +++ b/discord/poll.go @@ -0,0 +1,69 @@ +package discord + +import ( + "time" + + "github.com/disgoorg/json" +) + +type Poll struct { + Question PollMedia `json:"question"` + Answers []PollAnswer `json:"answers"` + Expiry *time.Time `json:"expiry"` + AllowMultiselect bool `json:"allow_multiselect"` + LayoutType PollLayoutType `json:"layout_type"` + Results *PollResults `json:"results"` +} + +type PollCreate struct { + Question PollMedia `json:"question"` + Answers []PollMedia `json:"-"` + Duration int `json:"duration"` + AllowMultiselect bool `json:"allow_multiselect"` + LayoutType PollLayoutType `json:"layout_type,omitempty"` +} + +func (p PollCreate) MarshalJSON() ([]byte, error) { + type pollCreate PollCreate + + answers := make([]PollAnswer, 0, len(p.Answers)) + for _, answer := range p.Answers { + answers = append(answers, PollAnswer{ + PollMedia: answer, + }) + } + return json.Marshal(struct { + Answers []PollAnswer `json:"answers"` + pollCreate + }{ + Answers: answers, + pollCreate: pollCreate(p), + }) +} + +type PollMedia struct { + Text *string `json:"text"` + Emoji *PartialEmoji `json:"emoji,omitempty"` +} + +type PollAnswer struct { + AnswerID *int `json:"answer_id,omitempty"` + PollMedia PollMedia `json:"poll_media"` +} + +type PollResults struct { + IsFinalized bool `json:"is_finalized"` + AnswerCounts []PollAnswerCount `json:"answer_counts"` +} + +type PollAnswerCount struct { + ID int `json:"id"` + Count int `json:"count"` + MeVoted bool `json:"me_voted"` +} + +type PollLayoutType int + +const ( + PollLayoutTypeDefault PollLayoutType = iota + 1 +) diff --git a/discord/poll_create_builder.go b/discord/poll_create_builder.go new file mode 100644 index 000000000..9e00b813a --- /dev/null +++ b/discord/poll_create_builder.go @@ -0,0 +1,66 @@ +package discord + +// PollCreateBuilder helps create PollCreate structs easier +type PollCreateBuilder struct { + PollCreate +} + +// SetQuestion sets the question of the Poll +func (b *PollCreateBuilder) SetQuestion(text string) *PollCreateBuilder { + b.Question = PollMedia{ + Text: &text, + } + return b +} + +// SetAnswers sets the answers of the Poll +func (b *PollCreateBuilder) SetAnswers(answers ...PollMedia) *PollCreateBuilder { + b.Answers = answers + return b +} + +// AddAnswer adds an answer to the Poll +func (b *PollCreateBuilder) AddAnswer(text string, emoji *PartialEmoji) *PollCreateBuilder { + b.Answers = append(b.Answers, PollMedia{ + Text: &text, + Emoji: emoji, + }) + return b +} + +// RemoveAnswer removes an answer from the Poll +func (b *PollCreateBuilder) RemoveAnswer(i int) *PollCreateBuilder { + if len(b.Answers) > i { + b.Answers = append(b.Answers[:i], b.Answers[i+1:]...) + } + return b +} + +// ClearAnswers removes all answers of the Poll +func (b *PollCreateBuilder) ClearAnswers() *PollCreateBuilder { + b.Answers = []PollMedia{} + return b +} + +// SetDuration sets the duration of the Poll (in hours) +func (b *PollCreateBuilder) SetDuration(duration int) *PollCreateBuilder { + b.Duration = duration + return b +} + +// SetAllowMultiselect sets whether users will be able to vote for more than one answer of the Poll +func (b *PollCreateBuilder) SetAllowMultiselect(multiselect bool) *PollCreateBuilder { + b.AllowMultiselect = multiselect + return b +} + +// SetLayoutType sets the layout of the Poll +func (b *PollCreateBuilder) SetLayoutType(layout PollLayoutType) *PollCreateBuilder { + b.LayoutType = layout + return b +} + +// Build builds the PollCreateBuilder to a PollCreate struct +func (b *PollCreateBuilder) Build() PollCreate { + return b.PollCreate +} diff --git a/discord/select_menu.go b/discord/select_menu.go index e4db9a9d3..6f7105dee 100644 --- a/discord/select_menu.go +++ b/discord/select_menu.go @@ -1,6 +1,9 @@ package discord -import "github.com/disgoorg/json" +import ( + "github.com/disgoorg/json" + "github.com/disgoorg/snowflake/v2" +) type SelectMenuComponent interface { InteractiveComponent @@ -189,11 +192,12 @@ func NewUserSelectMenu(customID string, placeholder string) UserSelectMenuCompon } type UserSelectMenuComponent struct { - CustomID string `json:"custom_id"` - Placeholder string `json:"placeholder,omitempty"` - MinValues *int `json:"min_values,omitempty"` - MaxValues int `json:"max_values,omitempty"` - Disabled bool `json:"disabled,omitempty"` + CustomID string `json:"custom_id"` + Placeholder string `json:"placeholder,omitempty"` + DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` + MinValues *int `json:"min_values,omitempty"` + MaxValues int `json:"max_values,omitempty"` + Disabled bool `json:"disabled,omitempty"` } func (c UserSelectMenuComponent) MarshalJSON() ([]byte, error) { @@ -261,6 +265,30 @@ func (c UserSelectMenuComponent) WithDisabled(disabled bool) UserSelectMenuCompo return c } +// SetDefaultValues returns a new UserSelectMenuComponent with the provided default values +func (c UserSelectMenuComponent) SetDefaultValues(defaultValues ...snowflake.ID) UserSelectMenuComponent { + values := make([]SelectMenuDefaultValue, 0, len(defaultValues)) + for _, value := range defaultValues { + values = append(values, NewSelectMenuDefaultUser(value)) + } + c.DefaultValues = values + return c +} + +// AddDefaultValue returns a new UserSelectMenuComponent with the provided default value added +func (c UserSelectMenuComponent) AddDefaultValue(defaultValue snowflake.ID) UserSelectMenuComponent { + c.DefaultValues = append(c.DefaultValues, NewSelectMenuDefaultUser(defaultValue)) + return c +} + +// RemoveDefaultValue returns a new UserSelectMenuComponent with the provided default value at the index removed +func (c UserSelectMenuComponent) RemoveDefaultValue(index int) UserSelectMenuComponent { + if len(c.DefaultValues) > index { + c.DefaultValues = append(c.DefaultValues[:index], c.DefaultValues[index+1:]...) + } + return c +} + var ( _ Component = (*UserSelectMenuComponent)(nil) _ InteractiveComponent = (*UserSelectMenuComponent)(nil) @@ -276,11 +304,12 @@ func NewRoleSelectMenu(customID string, placeholder string) RoleSelectMenuCompon } type RoleSelectMenuComponent struct { - CustomID string `json:"custom_id"` - Placeholder string `json:"placeholder,omitempty"` - MinValues *int `json:"min_values,omitempty"` - MaxValues int `json:"max_values,omitempty"` - Disabled bool `json:"disabled,omitempty"` + CustomID string `json:"custom_id"` + Placeholder string `json:"placeholder,omitempty"` + DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` + MinValues *int `json:"min_values,omitempty"` + MaxValues int `json:"max_values,omitempty"` + Disabled bool `json:"disabled,omitempty"` } func (c RoleSelectMenuComponent) MarshalJSON() ([]byte, error) { @@ -348,6 +377,30 @@ func (c RoleSelectMenuComponent) WithDisabled(disabled bool) RoleSelectMenuCompo return c } +// SetDefaultValues returns a new RoleSelectMenuComponent with the provided default values +func (c RoleSelectMenuComponent) SetDefaultValues(defaultValues ...snowflake.ID) RoleSelectMenuComponent { + values := make([]SelectMenuDefaultValue, 0, len(defaultValues)) + for _, value := range defaultValues { + values = append(values, NewSelectMenuDefaultRole(value)) + } + c.DefaultValues = values + return c +} + +// AddDefaultValue returns a new RoleSelectMenuComponent with the provided default value added +func (c RoleSelectMenuComponent) AddDefaultValue(defaultValue snowflake.ID) RoleSelectMenuComponent { + c.DefaultValues = append(c.DefaultValues, NewSelectMenuDefaultRole(defaultValue)) + return c +} + +// RemoveDefaultValue returns a new RoleSelectMenuComponent with the provided default value at the index removed +func (c RoleSelectMenuComponent) RemoveDefaultValue(index int) RoleSelectMenuComponent { + if len(c.DefaultValues) > index { + c.DefaultValues = append(c.DefaultValues[:index], c.DefaultValues[index+1:]...) + } + return c +} + var ( _ Component = (*MentionableSelectMenuComponent)(nil) _ InteractiveComponent = (*MentionableSelectMenuComponent)(nil) @@ -363,11 +416,12 @@ func NewMentionableSelectMenu(customID string, placeholder string) MentionableSe } type MentionableSelectMenuComponent struct { - CustomID string `json:"custom_id"` - Placeholder string `json:"placeholder,omitempty"` - MinValues *int `json:"min_values,omitempty"` - MaxValues int `json:"max_values,omitempty"` - Disabled bool `json:"disabled,omitempty"` + CustomID string `json:"custom_id"` + Placeholder string `json:"placeholder,omitempty"` + DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` + MinValues *int `json:"min_values,omitempty"` + MaxValues int `json:"max_values,omitempty"` + Disabled bool `json:"disabled,omitempty"` } func (c MentionableSelectMenuComponent) MarshalJSON() ([]byte, error) { @@ -435,6 +489,27 @@ func (c MentionableSelectMenuComponent) WithDisabled(disabled bool) MentionableS return c } +// SetDefaultValues returns a new MentionableSelectMenuComponent with the provided default values +func (c MentionableSelectMenuComponent) SetDefaultValues(defaultValues ...SelectMenuDefaultValue) MentionableSelectMenuComponent { + c.DefaultValues = defaultValues + return c +} + +// AddDefaultValue returns a new MentionableSelectMenuComponent with the provided default value added. +// SelectMenuDefaultValue can easily be constructed using helpers like NewSelectMenuDefaultUser or NewSelectMenuDefaultRole +func (c MentionableSelectMenuComponent) AddDefaultValue(value SelectMenuDefaultValue) MentionableSelectMenuComponent { + c.DefaultValues = append(c.DefaultValues, value) + return c +} + +// RemoveDefaultValue returns a new MentionableSelectMenuComponent with the provided default value at the index removed +func (c MentionableSelectMenuComponent) RemoveDefaultValue(index int) MentionableSelectMenuComponent { + if len(c.DefaultValues) > index { + c.DefaultValues = append(c.DefaultValues[:index], c.DefaultValues[index+1:]...) + } + return c +} + var ( _ Component = (*ChannelSelectMenuComponent)(nil) _ InteractiveComponent = (*ChannelSelectMenuComponent)(nil) @@ -450,12 +525,13 @@ func NewChannelSelectMenu(customID string, placeholder string) ChannelSelectMenu } type ChannelSelectMenuComponent struct { - CustomID string `json:"custom_id"` - Placeholder string `json:"placeholder,omitempty"` - MinValues *int `json:"min_values,omitempty"` - MaxValues int `json:"max_values,omitempty"` - Disabled bool `json:"disabled,omitempty"` - ChannelTypes []ChannelType `json:"channel_types,omitempty"` + CustomID string `json:"custom_id"` + Placeholder string `json:"placeholder,omitempty"` + DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"` + MinValues *int `json:"min_values,omitempty"` + MaxValues int `json:"max_values,omitempty"` + Disabled bool `json:"disabled,omitempty"` + ChannelTypes []ChannelType `json:"channel_types,omitempty"` } func (c ChannelSelectMenuComponent) MarshalJSON() ([]byte, error) { @@ -528,3 +604,64 @@ func (c ChannelSelectMenuComponent) WithChannelTypes(channelTypes ...ChannelType c.ChannelTypes = channelTypes return c } + +// SetDefaultValues returns a new ChannelSelectMenuComponent with the provided default values +func (c ChannelSelectMenuComponent) SetDefaultValues(defaultValues ...snowflake.ID) ChannelSelectMenuComponent { + values := make([]SelectMenuDefaultValue, 0, len(defaultValues)) + for _, value := range defaultValues { + values = append(values, NewSelectMenuDefaultChannel(value)) + } + c.DefaultValues = values + return c +} + +// AddDefaultValue returns a new ChannelSelectMenuComponent with the provided default value added +func (c ChannelSelectMenuComponent) AddDefaultValue(defaultValue snowflake.ID) ChannelSelectMenuComponent { + c.DefaultValues = append(c.DefaultValues, NewSelectMenuDefaultChannel(defaultValue)) + return c +} + +// RemoveDefaultValue returns a new ChannelSelectMenuComponent with the provided default value at the index removed +func (c ChannelSelectMenuComponent) RemoveDefaultValue(index int) ChannelSelectMenuComponent { + if len(c.DefaultValues) > index { + c.DefaultValues = append(c.DefaultValues[:index], c.DefaultValues[index+1:]...) + } + return c +} + +type SelectMenuDefaultValue struct { + ID snowflake.ID `json:"id"` + Type SelectMenuDefaultValueType `json:"type"` +} + +type SelectMenuDefaultValueType string + +const ( + SelectMenuDefaultValueTypeUser SelectMenuDefaultValueType = "user" + SelectMenuDefaultValueTypeRole SelectMenuDefaultValueType = "role" + SelectMenuDefaultValueTypeChannel SelectMenuDefaultValueType = "channel" +) + +// NewSelectMenuDefaultUser returns a new SelectMenuDefaultValue of type SelectMenuDefaultValueTypeUser +func NewSelectMenuDefaultUser(id snowflake.ID) SelectMenuDefaultValue { + return SelectMenuDefaultValue{ + ID: id, + Type: SelectMenuDefaultValueTypeUser, + } +} + +// NewSelectMenuDefaultRole returns a new SelectMenuDefaultValue of type SelectMenuDefaultValueTypeRole +func NewSelectMenuDefaultRole(id snowflake.ID) SelectMenuDefaultValue { + return SelectMenuDefaultValue{ + ID: id, + Type: SelectMenuDefaultValueTypeRole, + } +} + +// NewSelectMenuDefaultChannel returns a new SelectMenuDefaultValue of type SelectMenuDefaultValueTypeChannel +func NewSelectMenuDefaultChannel(id snowflake.ID) SelectMenuDefaultValue { + return SelectMenuDefaultValue{ + ID: id, + Type: SelectMenuDefaultValueTypeChannel, + } +} diff --git a/discord/sku.go b/discord/sku.go new file mode 100644 index 000000000..4e34ea191 --- /dev/null +++ b/discord/sku.go @@ -0,0 +1,44 @@ +package discord + +import ( + "time" + + "github.com/disgoorg/snowflake/v2" +) + +type SKU struct { + ID snowflake.ID `json:"id"` + Type SKUType `json:"type"` + ApplicationID snowflake.ID `json:"application_id"` + Name string `json:"name"` + Slug string `json:"slug"` + DependentSkuID *snowflake.ID `json:"dependent_sku_id"` + AccessType int `json:"access_type"` + Features []string `json:"features"` + ReleaseDate *time.Time `json:"release_date"` + Premium bool `json:"premium"` + Flags SKUFlags `json:"flags"` + ShowAgeGate bool `json:"show_age_gate"` +} + +type SKUType int + +const ( + SKUTypeDurable SKUType = iota + 2 + SKUTypeConsumable + _ + SKUTypeSubscription + SKUTypeSubscriptionGroup +) + +type SKUFlags int + +const ( + SKUFlagAvailable SKUFlags = 1 << (iota + 2) + _ + _ + _ + _ + SKUFlagGuildSubscription + SKUFlagUserSubscription +) diff --git a/discord/sound.go b/discord/sound.go new file mode 100644 index 000000000..0bd2908fe --- /dev/null +++ b/discord/sound.go @@ -0,0 +1,59 @@ +package discord + +import ( + "encoding/base64" + "fmt" + "io" + + "github.com/disgoorg/json" +) + +type SoundType string + +const ( + SoundTypeMP3 SoundType = "audio/mpeg" + SoundTypeOGG SoundType = "audio/ogg" + SoundTypeWAV SoundType = "audio/wav" + SoundTypeUnknown = SoundTypeMP3 +) + +func (t SoundType) MIME() string { + return string(t) +} + +func (t SoundType) Header() string { + return "data:" + string(t) + ";base64" +} + +var _ json.Marshaler = (*Sound)(nil) +var _ fmt.Stringer = (*Sound)(nil) + +func NewSound(soundType SoundType, reader io.Reader) (*Sound, error) { + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + return NewSoundRaw(soundType, data), nil +} + +func NewSoundRaw(soundType SoundType, src []byte) *Sound { + data := make([]byte, base64.StdEncoding.EncodedLen(len(src))) + base64.StdEncoding.Encode(data, src) + return &Sound{Type: soundType, Data: data} +} + +type Sound struct { + Type SoundType + Data []byte +} + +func (s Sound) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s Sound) String() string { + if len(s.Data) == 0 { + return "" + } + return s.Type.Header() + "," + string(s.Data) +} diff --git a/discord/soundboard.go b/discord/soundboard.go new file mode 100644 index 000000000..0dc4b72ac --- /dev/null +++ b/discord/soundboard.go @@ -0,0 +1,48 @@ +package discord + +import ( + "github.com/disgoorg/json" + "github.com/disgoorg/snowflake/v2" +) + +type VoiceChannelEffectAnimationType int + +const ( + VoiceChannelEffectAnimationTypePremium VoiceChannelEffectAnimationType = iota + VoiceChannelEffectAnimationTypeBasic +) + +type SoundboardSound struct { + Name string `json:"name"` + SoundID snowflake.ID `json:"sound_id"` + Volume float64 `json:"volume"` + EmojiID *snowflake.ID `json:"emoji_id"` + EmojiName *string `json:"emoji_name"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Available *bool `json:"available,omitempty"` + User *User `json:"user,omitempty"` +} + +func (s SoundboardSound) URL(opts ...CDNOpt) string { + return formatAssetURL(SoundboardSoundFile, opts, s.SoundID) +} + +type SoundboardSoundCreate struct { + Name string `json:"name"` + Sound Sound `json:"sound"` + Volume *float64 `json:"volume,omitempty"` + EmojiID snowflake.ID `json:"emoji_id,omitempty"` + EmojiName string `json:"emoji_name,omitempty"` +} + +type SoundboardSoundUpdate struct { + Name *string `json:"name,omitempty"` + Volume *json.Nullable[float64] `json:"volume,omitempty"` + EmojiID *json.Nullable[snowflake.ID] `json:"emoji_id,omitempty"` + EmojiName *json.Nullable[string] `json:"emoji_name,omitempty"` +} + +type SendSoundboardSound struct { + SoundID snowflake.ID `json:"sound_id"` + SourceGuildID *snowflake.ID `json:"source_guild_id,omitempty"` +} diff --git a/discord/stage_instance.go b/discord/stage_instance.go index 4566fc72f..588c80b62 100644 --- a/discord/stage_instance.go +++ b/discord/stage_instance.go @@ -31,6 +31,7 @@ type StageInstanceCreate struct { Topic string `json:"topic,omitempty"` PrivacyLevel StagePrivacyLevel `json:"privacy_level,omitempty"` SendStartNotification bool `json:"send_start_notification,omitempty"` + GuildScheduledEventID snowflake.ID `json:"guild_scheduled_event_id,omitempty"` } type StageInstanceUpdate struct { diff --git a/discord/subscription.go b/discord/subscription.go new file mode 100644 index 000000000..309418d4d --- /dev/null +++ b/discord/subscription.go @@ -0,0 +1,28 @@ +package discord + +import ( + "time" + + "github.com/disgoorg/snowflake/v2" +) + +type Subscription struct { + ID snowflake.ID `json:"id"` + UserID snowflake.ID `json:"user_id"` + SkuIDs []snowflake.ID `json:"sku_ids"` + EntitlementIDs []snowflake.ID `json:"entitlement_ids"` + RenewalSkuIDs []snowflake.ID `json:"renewal_sku_ids"` + CurrentPeriodStart time.Time `json:"current_period_start"` + CurrentPeriodEnd time.Time `json:"current_period_end"` + Status SubscriptionStatus `json:"status"` + CanceledAt *time.Time `json:"canceled_at"` + Country *string `json:"country"` +} + +type SubscriptionStatus int + +const ( + SubscriptionStatusActive SubscriptionStatus = iota + SubscriptionStatusEnding + SubscriptionStatusInactive +) diff --git a/discord/thread.go b/discord/thread.go index 1beae1014..5a6c19da5 100644 --- a/discord/thread.go +++ b/discord/thread.go @@ -80,7 +80,7 @@ func (GuildPublicThreadCreate) Type() ChannelType { type GuildPrivateThreadCreate struct { Name string `json:"name"` AutoArchiveDuration AutoArchiveDuration `json:"auto_archive_duration,omitempty"` - Invitable bool `json:"invitable,omitempty"` + Invitable *bool `json:"invitable,omitempty"` } func (c GuildPrivateThreadCreate) MarshalJSON() ([]byte, error) { diff --git a/discord/user.go b/discord/user.go index caa63b8d5..bdf74c4f0 100644 --- a/discord/user.go +++ b/discord/user.go @@ -65,17 +65,17 @@ var _ Mentionable = (*User)(nil) // User is a struct for interacting with discord's users type User struct { - ID snowflake.ID `json:"id"` - Username string `json:"username"` - Discriminator string `json:"discriminator"` - GlobalName *string `json:"global_name"` - Avatar *string `json:"avatar"` - Banner *string `json:"banner"` - AccentColor *int `json:"accent_color"` - Bot bool `json:"bot"` - System bool `json:"system"` - PublicFlags UserFlags `json:"public_flags"` - AvatarDecoration *string `json:"avatar_decoration"` + ID snowflake.ID `json:"id"` + Username string `json:"username"` + Discriminator string `json:"discriminator"` + GlobalName *string `json:"global_name"` + Avatar *string `json:"avatar"` + Banner *string `json:"banner"` + AccentColor *int `json:"accent_color"` + Bot bool `json:"bot"` + System bool `json:"system"` + PublicFlags UserFlags `json:"public_flags"` + AvatarDecorationData *AvatarDecorationData `json:"avatar_decoration_data"` } // String returns a mention of the user @@ -106,10 +106,7 @@ func (u User) EffectiveAvatarURL(opts ...CDNOpt) string { if u.Avatar == nil { return u.DefaultAvatarURL(opts...) } - if avatar := u.AvatarURL(opts...); avatar != nil { - return *avatar - } - return "" + return formatAssetURL(UserAvatar, opts, u.ID, *u.Avatar) } // AvatarURL returns the avatar URL of the user if set or nil @@ -145,10 +142,10 @@ func (u User) BannerURL(opts ...CDNOpt) *string { // AvatarDecorationURL returns the avatar decoration URL if set or nil func (u User) AvatarDecorationURL(opts ...CDNOpt) *string { - if u.AvatarDecoration == nil { + if u.AvatarDecorationData == nil { return nil } - url := formatAssetURL(UserAvatarDecoration, opts, u.ID, *u.AvatarDecoration) + url := formatAssetURL(AvatarDecoration, opts, u.AvatarDecorationData.Asset) return &url } @@ -181,10 +178,11 @@ const ( PremiumTypeNitroBasic ) -// SelfUserUpdate is the payload used to update the OAuth2User -type SelfUserUpdate struct { +// UserUpdate is the payload used to update the OAuth2User +type UserUpdate struct { Username string `json:"username,omitempty"` Avatar *json.Nullable[Icon] `json:"avatar,omitempty"` + Banner *json.Nullable[Icon] `json:"banner,omitempty"` } type ApplicationRoleConnection struct { @@ -198,3 +196,8 @@ type ApplicationRoleConnectionUpdate struct { PlatformUsername *string `json:"platform_username,omitempty"` Metadata *map[string]string `json:"metadata,omitempty"` } + +type AvatarDecorationData struct { + Asset string `json:"asset"` + SkuID snowflake.ID `json:"sku_id"` +} diff --git a/discord/voice_region.go b/discord/voice_region.go index baf0dd61a..59561152e 100644 --- a/discord/voice_region.go +++ b/discord/voice_region.go @@ -1,10 +1,8 @@ package discord -import "github.com/disgoorg/snowflake/v2" - // VoiceRegion (https://discord.com/developers/docs/resources/voice#voice-region-object) type VoiceRegion struct { - ID snowflake.ID `json:"id"` + ID string `json:"id"` Name string `json:"name"` Vip bool `json:"vip"` Optimal bool `json:"optimal"` diff --git a/discord/webhook_message_create.go b/discord/webhook_message_create.go index a1df9508f..f84e4ea8d 100644 --- a/discord/webhook_message_create.go +++ b/discord/webhook_message_create.go @@ -1,5 +1,7 @@ package discord +import "github.com/disgoorg/snowflake/v2" + type WebhookMessageCreate struct { Content string `json:"content,omitempty"` Username string `json:"username,omitempty"` @@ -12,6 +14,8 @@ type WebhookMessageCreate struct { AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` Flags MessageFlags `json:"flags,omitempty"` ThreadName string `json:"thread_name,omitempty"` + AppliedTags []snowflake.ID `json:"applied_tags,omitempty"` + Poll *PollCreate `json:"poll,omitempty"` } // ToBody returns the MessageCreate ready for body diff --git a/discord/webhook_message_create_builder.go b/discord/webhook_message_create_builder.go index e82bbb958..4c8666959 100644 --- a/discord/webhook_message_create_builder.go +++ b/discord/webhook_message_create_builder.go @@ -210,6 +210,18 @@ func (b *WebhookMessageCreateBuilder) SetThreadName(threadName string) *WebhookM return b } +// SetPoll sets the Poll of the webhook Message +func (b *WebhookMessageCreateBuilder) SetPoll(poll PollCreate) *WebhookMessageCreateBuilder { + b.Poll = &poll + return b +} + +// ClearPoll clears the Poll of the webhook Message +func (b *WebhookMessageCreateBuilder) ClearPoll() *WebhookMessageCreateBuilder { + b.Poll = nil + return b +} + // Build builds the WebhookMessageCreateBuilder to a MessageCreate struct func (b *WebhookMessageCreateBuilder) Build() WebhookMessageCreate { b.WebhookMessageCreate.Components = b.Components diff --git a/discord/webhook_message_update.go b/discord/webhook_message_update.go index c2733f99e..c756d1d39 100644 --- a/discord/webhook_message_update.go +++ b/discord/webhook_message_update.go @@ -8,6 +8,7 @@ type WebhookMessageUpdate struct { Attachments *[]AttachmentUpdate `json:"attachments,omitempty"` Files []*File `json:"-"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` + Poll *PollCreate `json:"poll,omitempty"` } // ToBody returns the WebhookMessageUpdate ready for body diff --git a/discord/webhook_message_update_builder.go b/discord/webhook_message_update_builder.go index 1fcd01ffe..b031fa642 100644 --- a/discord/webhook_message_update_builder.go +++ b/discord/webhook_message_update_builder.go @@ -211,6 +211,18 @@ func (b *WebhookMessageUpdateBuilder) ClearAllowedMentions() *WebhookMessageUpda return b.SetAllowedMentions(nil) } +// SetPoll sets the Poll of the webhook Message +func (b *WebhookMessageUpdateBuilder) SetPoll(poll PollCreate) *WebhookMessageUpdateBuilder { + b.Poll = &poll + return b +} + +// ClearPoll clears the Poll of the webhook Message +func (b *WebhookMessageUpdateBuilder) ClearPoll() *WebhookMessageUpdateBuilder { + b.Poll = nil + return b +} + // Build builds the WebhookMessageUpdateBuilder to a MessageUpdate struct func (b *WebhookMessageUpdateBuilder) Build() WebhookMessageUpdate { return b.WebhookMessageUpdate diff --git a/disgo.go b/disgo.go index ec05f01ae..cd494f41c 100644 --- a/disgo.go +++ b/disgo.go @@ -48,7 +48,6 @@ package disgo import ( "runtime" "runtime/debug" - "strings" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/handlers" @@ -67,8 +66,7 @@ var ( // Version is the currently used version of DisGo Version = getVersion() - // OS is the currently used OS - OS = getOS() + SemVersion = "semver:" + Version ) func getVersion() string { @@ -83,27 +81,16 @@ func getVersion() string { return "unknown" } -func getOS() string { - os := runtime.GOOS - if strings.HasPrefix(os, "windows") { - return "windows" - } - if strings.HasPrefix(os, "darwin") { - return "darwin" - } - return "linux" -} - // New creates a new bot.Client with the provided token & bot.ConfigOpt(s) func New(token string, opts ...bot.ConfigOpt) (bot.Client, error) { config := bot.DefaultConfig(handlers.GetGatewayHandlers(), handlers.GetHTTPServerHandler()) config.Apply(opts) return bot.BuildClient(token, - *config, + config, handlers.DefaultGatewayEventHandlerFunc, handlers.DefaultHTTPServerEventHandlerFunc, - OS, + runtime.GOOS, Name, GitHub, Version, diff --git a/events/dm_message_poll_events.go b/events/dm_message_poll_events.go new file mode 100644 index 000000000..a80db4a6a --- /dev/null +++ b/events/dm_message_poll_events.go @@ -0,0 +1,24 @@ +package events + +import ( + "github.com/disgoorg/snowflake/v2" +) + +// GenericDMMessagePollVote is called upon receiving DMMessagePollVoteAdd or DMMessagePollVoteRemove (requires gateway.IntentDirectMessagePolls) +type GenericDMMessagePollVote struct { + *GenericEvent + UserID snowflake.ID + ChannelID snowflake.ID + MessageID snowflake.ID + AnswerID int +} + +// DMMessagePollVoteAdd indicates that a discord.User voted on a discord.Poll in a DM (requires gateway.IntentDirectMessagePolls) +type DMMessagePollVoteAdd struct { + *GenericDMMessagePollVote +} + +// DMMessagePollVoteRemove indicates that a discord.User removed their vote on a discord.Poll in a DM (requires gateway.IntentDirectMessagePolls) +type DMMessagePollVoteRemove struct { + *GenericDMMessagePollVote +} diff --git a/events/entitlement_events.go b/events/entitlement_events.go new file mode 100644 index 000000000..68d79931b --- /dev/null +++ b/events/entitlement_events.go @@ -0,0 +1,20 @@ +package events + +import "github.com/disgoorg/disgo/discord" + +type GenericEntitlementEvent struct { + *GenericEvent + discord.Entitlement +} + +type EntitlementCreate struct { + *GenericEntitlementEvent +} + +type EntitlementUpdate struct { + *GenericEntitlementEvent +} + +type EntitlementDelete struct { + *GenericEntitlementEvent +} diff --git a/events/guild_emoji_events.go b/events/guild_emoji_events.go index 8ffa30d22..80d97235b 100644 --- a/events/guild_emoji_events.go +++ b/events/guild_emoji_events.go @@ -14,25 +14,25 @@ type EmojisUpdate struct { gateway.EventGuildEmojisUpdate } -// GenericEmoji is called upon receiving EmojiCreate , EmojiUpdate or EmojiDelete (requires gateway.IntentGuildEmojisAndStickers) +// GenericEmoji is called upon receiving EmojiCreate , EmojiUpdate or EmojiDelete (requires gateway.IntentGuildExpressions) type GenericEmoji struct { *GenericEvent GuildID snowflake.ID Emoji discord.Emoji } -// EmojiCreate indicates that a new discord.Emoji got created in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// EmojiCreate indicates that a new discord.Emoji got created in a discord.Guild (requires gateway.IntentGuildExpressions) type EmojiCreate struct { *GenericEmoji } -// EmojiUpdate indicates that a discord.Emoji got updated in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// EmojiUpdate indicates that a discord.Emoji got updated in a discord.Guild (requires gateway.IntentGuildExpressions) type EmojiUpdate struct { *GenericEmoji OldEmoji discord.Emoji } -// EmojiDelete indicates that a discord.Emoji got deleted in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// EmojiDelete indicates that a discord.Emoji got deleted in a discord.Guild (requires gateway.IntentGuildExpressions) type EmojiDelete struct { *GenericEmoji } diff --git a/events/guild_invite_events.go b/events/guild_invite_events.go index 756bf22c4..095ff9189 100644 --- a/events/guild_invite_events.go +++ b/events/guild_invite_events.go @@ -4,28 +4,45 @@ import ( "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/gateway" ) -// GenericInvite is called upon receiving InviteCreate or InviteDelete (requires gateway.IntentGuildInvites) -type GenericInvite struct { +// InviteCreate is called upon creation of a new discord.Invite (requires gateway.IntentGuildInvites) +type InviteCreate struct { *GenericEvent - GuildID *snowflake.ID - ChannelID snowflake.ID - Code string + + gateway.EventInviteCreate } // Channel returns the discord.GuildChannel the GenericInvite happened in. -func (e *GenericInvite) Channel() (discord.GuildChannel, bool) { +func (e *InviteCreate) Channel() (discord.GuildChannel, bool) { return e.Client().Caches().Channel(e.ChannelID) } -// InviteCreate is called upon creation of a new discord.Invite (requires gateway.IntentGuildInvites) -type InviteCreate struct { - *GenericInvite - Invite discord.Invite +func (e *InviteCreate) Guild() (discord.Guild, bool) { + if e.GuildID == nil { + return discord.Guild{}, false + } + return e.Client().Caches().Guild(*e.GuildID) } // InviteDelete is called upon deletion of a discord.Invite (requires gateway.IntentGuildInvites) type InviteDelete struct { - *GenericInvite + *GenericEvent + + GuildID *snowflake.ID + ChannelID snowflake.ID + Code string +} + +// Channel returns the discord.GuildChannel the GenericInvite happened in. +func (e *InviteDelete) Channel() (discord.GuildChannel, bool) { + return e.Client().Caches().Channel(e.ChannelID) +} + +func (e *InviteDelete) Guild() (discord.Guild, bool) { + if e.GuildID == nil { + return discord.Guild{}, false + } + return e.Client().Caches().Guild(*e.GuildID) } diff --git a/events/guild_message_poll_events.go b/events/guild_message_poll_events.go new file mode 100644 index 000000000..9c9829ceb --- /dev/null +++ b/events/guild_message_poll_events.go @@ -0,0 +1,36 @@ +package events + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" +) + +// GenericGuildMessagePollVote is called upon receiving GuildMessagePollVoteAdd or GuildMessagePollVoteRemove (requires gateway.IntentGuildMessagePolls) +type GenericGuildMessagePollVote struct { + *GenericEvent + UserID snowflake.ID + ChannelID snowflake.ID + MessageID snowflake.ID + GuildID snowflake.ID + AnswerID int +} + +// Guild returns the discord.Guild where the GenericGuildMessagePollVote happened +func (e *GenericGuildMessagePollVote) Guild() (discord.Guild, bool) { + return e.Client().Caches().Guild(e.GuildID) +} + +// Channel returns the discord.GuildMessageChannel where the GenericGuildMessagePollVote happened +func (e *GenericGuildMessagePollVote) Channel() (discord.GuildMessageChannel, bool) { + return e.Client().Caches().GuildMessageChannel(e.ChannelID) +} + +// GuildMessagePollVoteAdd indicates that a discord.User voted on a discord.Poll in a discord.Guild (requires gateway.IntentGuildMessagePolls) +type GuildMessagePollVoteAdd struct { + *GenericGuildMessagePollVote +} + +// GuildMessagePollVoteRemove indicates that a discord.User removed their vote on a discord.Poll in a discord.Guild (requires gateway.IntentGuildMessagePolls) +type GuildMessagePollVoteRemove struct { + *GenericGuildMessagePollVote +} diff --git a/events/guild_soundboard_events.go b/events/guild_soundboard_events.go new file mode 100644 index 000000000..8064fd9dc --- /dev/null +++ b/events/guild_soundboard_events.go @@ -0,0 +1,44 @@ +package events + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" +) + +// GenericGuildSoundboardSound is called upon receiving GuildSoundboardSoundCreate and GuildSoundboardSoundUpdate (requires gateway.IntentGuildExpressions) +type GenericGuildSoundboardSound struct { + *GenericEvent + discord.SoundboardSound +} + +// GuildSoundboardSoundCreate indicates that a discord.SoundboardSound was created in a discord.Guild (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundCreate struct { + *GenericGuildSoundboardSound +} + +// GuildSoundboardSoundUpdate indicates that a discord.SoundboardSound was updated in a discord.Guild (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundUpdate struct { + *GenericGuildSoundboardSound + OldGuildSoundboardSound discord.SoundboardSound +} + +// GuildSoundboardSoundDelete indicates that a discord.SoundboardSound was deleted in a discord.Guild (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundDelete struct { + *GenericEvent + SoundID snowflake.ID + GuildID snowflake.ID +} + +// GuildSoundboardSoundsUpdate indicates when multiple discord.Guild soundboard sounds were updated (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundsUpdate struct { + *GenericEvent + SoundboardSounds []discord.SoundboardSound + GuildID snowflake.ID +} + +// SoundboardSounds is a response to gateway.OpcodeRequestSoundboardSounds +type SoundboardSounds struct { + *GenericEvent + SoundboardSounds []discord.SoundboardSound + GuildID snowflake.ID +} diff --git a/events/guild_sticker_events.go b/events/guild_sticker_events.go index 39f40ad3f..9984f5318 100644 --- a/events/guild_sticker_events.go +++ b/events/guild_sticker_events.go @@ -14,25 +14,25 @@ type StickersUpdate struct { gateway.EventGuildStickersUpdate } -// GenericSticker is called upon receiving StickerCreate , StickerUpdate or StickerDelete (requires gateway.IntentGuildEmojisAndStickers) +// GenericSticker is called upon receiving StickerCreate , StickerUpdate or StickerDelete (requires gateway.IntentGuildExpressions) type GenericSticker struct { *GenericEvent GuildID snowflake.ID Sticker discord.Sticker } -// StickerCreate indicates that a new discord.Sticker got created in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// StickerCreate indicates that a new discord.Sticker got created in a discord.Guild (requires gateway.IntentGuildExpressions) type StickerCreate struct { *GenericSticker } -// StickerUpdate indicates that a discord.Sticker got updated in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// StickerUpdate indicates that a discord.Sticker got updated in a discord.Guild (requires gateway.IntentGuildExpressions) type StickerUpdate struct { *GenericSticker OldSticker discord.Sticker } -// StickerDelete indicates that a discord.Sticker got deleted in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// StickerDelete indicates that a discord.Sticker got deleted in a discord.Guild (requires gateway.IntentGuildExpressions) type StickerDelete struct { *GenericSticker } diff --git a/events/guild_thread_events.go b/events/guild_thread_events.go index 479fade92..60be737b9 100644 --- a/events/guild_thread_events.go +++ b/events/guild_thread_events.go @@ -19,6 +19,7 @@ type GenericThread struct { type ThreadCreate struct { *GenericThread ThreadMember discord.ThreadMember + NewlyCreated bool } // ThreadUpdate is dispatched when a thread is updated. diff --git a/events/guild_voice_events.go b/events/guild_voice_events.go index bc3a5cbbb..3f8d4593f 100644 --- a/events/guild_voice_events.go +++ b/events/guild_voice_events.go @@ -5,31 +5,37 @@ import ( "github.com/disgoorg/disgo/gateway" ) -// GenericGuildVoiceState is called upon receiving GuildVoiceJoin , GuildVoiceMove , GuildVoiceLeave +// GuildVoiceChannelEffectSend indicates that a discord.Member sent an effect in a discord.GuildVoiceChannel (requires gateway.IntentGuildVoiceStates) +type GuildVoiceChannelEffectSend struct { + *GenericEvent + gateway.EventVoiceChannelEffectSend +} + +// GenericGuildVoiceState is called upon receiving GuildVoiceJoin, GuildVoiceMove and GuildVoiceLeave type GenericGuildVoiceState struct { *GenericEvent VoiceState discord.VoiceState Member discord.Member } -// GuildVoiceStateUpdate indicates that the discord.VoiceState of a discord.Member has updated(requires gateway.IntentsGuildVoiceStates) +// GuildVoiceStateUpdate indicates that the discord.VoiceState of a discord.Member has updated (requires gateway.IntentGuildVoiceStates) type GuildVoiceStateUpdate struct { *GenericGuildVoiceState OldVoiceState discord.VoiceState } -// GuildVoiceJoin indicates that a discord.Member joined a discord.Channel(requires gateway.IntentsGuildVoiceStates) +// GuildVoiceJoin indicates that a discord.Member joined a discord.GuildVoiceChannel (requires gateway.IntentGuildVoiceStates) type GuildVoiceJoin struct { *GenericGuildVoiceState } -// GuildVoiceMove indicates that a discord.Member moved a discord.Channel(requires gateway.IntentsGuildVoiceStates) +// GuildVoiceMove indicates that a discord.Member was moved to a different discord.GuildVoiceChannel (requires gateway.IntentGuildVoiceStates) type GuildVoiceMove struct { *GenericGuildVoiceState OldVoiceState discord.VoiceState } -// GuildVoiceLeave indicates that a discord.Member left a discord.Channel(requires gateway.IntentsGuildVoiceStates) +// GuildVoiceLeave indicates that a discord.Member left a discord.GuildVoiceChannel (requires gateway.IntentGuildVoiceStates) type GuildVoiceLeave struct { *GenericGuildVoiceState OldVoiceState discord.VoiceState diff --git a/events/guild_webhooks_update_events.go b/events/guild_webhooks_update_events.go index f1f44d95e..25857a76d 100644 --- a/events/guild_webhooks_update_events.go +++ b/events/guild_webhooks_update_events.go @@ -19,7 +19,7 @@ func (e *WebhooksUpdate) Guild() (discord.Guild, bool) { return e.Client().Caches().Guild(e.GuildId) } -// Channel returns the discord.GuildMessageChannel webhook was updated in. +// Channel returns the discord.GuildMessageChannel the webhook was updated in. // This will only return cached channels! func (e *WebhooksUpdate) Channel() (discord.GuildMessageChannel, bool) { return e.Client().Caches().GuildMessageChannel(e.ChannelID) diff --git a/events/interaction_events.go b/events/interaction_events.go index b5453a413..4767c1525 100644 --- a/events/interaction_events.go +++ b/events/interaction_events.go @@ -42,6 +42,25 @@ func (e *ApplicationCommandInteractionCreate) Guild() (discord.Guild, bool) { return discord.Guild{}, false } +// Acknowledge acknowledges the interaction. +// +// This is used strictly for acknowledging the HTTP interaction request from discord. This responds with 202 Accepted. +// +// When using this, your first http request must be [rest.Interactions.CreateInteractionResponse] or [rest.Interactions.CreateInteractionResponseWithCallback] +// +// This does not produce a visible loading state to the user. +// You are expected to send a new http request within 3 seconds to respond to the interaction. +// This allows you to gracefully handle errors with your sent response & access the resulting message. +// +// If you want to create a visible loading state, use DeferCreateMessage. +// +// Source docs: [Discord Source docs] +// +// [Discord Source docs]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback +func (e *ApplicationCommandInteractionCreate) Acknowledge(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeAcknowledge, nil, opts...) +} + // CreateMessage responds to the interaction with a new message. func (e *ApplicationCommandInteractionCreate) CreateMessage(messageCreate discord.MessageCreate, opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypeCreateMessage, messageCreate, opts...) @@ -56,11 +75,22 @@ func (e *ApplicationCommandInteractionCreate) DeferCreateMessage(ephemeral bool, return e.Respond(discord.InteractionResponseTypeDeferredCreateMessage, data, opts...) } -// CreateModal responds to the interaction with a new modal. -func (e *ApplicationCommandInteractionCreate) CreateModal(modalCreate discord.ModalCreate, opts ...rest.RequestOpt) error { +// Modal responds to the interaction with a new modal. +func (e *ApplicationCommandInteractionCreate) Modal(modalCreate discord.ModalCreate, opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypeModal, modalCreate, opts...) } +// Deprecated: Respond with a discord.ButtonStylePremium button instead. +// PremiumRequired responds to the interaction with an upgrade button if available. +func (e *ApplicationCommandInteractionCreate) PremiumRequired(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) +} + +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *ApplicationCommandInteractionCreate) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} + // ComponentInteractionCreate indicates that a new component interaction has been created. type ComponentInteractionCreate struct { *GenericEvent @@ -78,6 +108,25 @@ func (e *ComponentInteractionCreate) Guild() (discord.Guild, bool) { return discord.Guild{}, false } +// Acknowledge acknowledges the interaction. +// +// This is used strictly for acknowledging the HTTP interaction request from discord. This responds with 202 Accepted. +// +// When using this, your first http request must be [rest.Interactions.CreateInteractionResponse] or [rest.Interactions.CreateInteractionResponseWithCallback] +// +// This does not produce a visible loading state to the user. +// You are expected to send a new http request within 3 seconds to respond to the interaction. +// This allows you to gracefully handle errors with your sent response & access the resulting message. +// +// If you want to create a visible loading state, use DeferCreateMessage. +// +// Source docs: [Discord Source docs] +// +// [Discord Source docs]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback +func (e *ComponentInteractionCreate) Acknowledge(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeAcknowledge, nil, opts...) +} + // CreateMessage responds to the interaction with a new message. func (e *ComponentInteractionCreate) CreateMessage(messageCreate discord.MessageCreate, opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypeCreateMessage, messageCreate, opts...) @@ -102,11 +151,22 @@ func (e *ComponentInteractionCreate) DeferUpdateMessage(opts ...rest.RequestOpt) return e.Respond(discord.InteractionResponseTypeDeferredUpdateMessage, nil, opts...) } -// CreateModal responds to the interaction with a new modal. -func (e *ComponentInteractionCreate) CreateModal(modalCreate discord.ModalCreate, opts ...rest.RequestOpt) error { +// Modal responds to the interaction with a new modal. +func (e *ComponentInteractionCreate) Modal(modalCreate discord.ModalCreate, opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypeModal, modalCreate, opts...) } +// Deprecated: Respond with a discord.ButtonStylePremium button instead. +// PremiumRequired responds to the interaction with an upgrade button if available. +func (e *ComponentInteractionCreate) PremiumRequired(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) +} + +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *ComponentInteractionCreate) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} + // AutocompleteInteractionCreate indicates that a new autocomplete interaction has been created. type AutocompleteInteractionCreate struct { *GenericEvent @@ -124,9 +184,28 @@ func (e *AutocompleteInteractionCreate) Guild() (discord.Guild, bool) { return discord.Guild{}, false } -// Result responds to the interaction with a slice of choices. -func (e *AutocompleteInteractionCreate) Result(choices []discord.AutocompleteChoice, opts ...rest.RequestOpt) error { - return e.Respond(discord.InteractionResponseTypeApplicationCommandAutocompleteResult, discord.AutocompleteResult{Choices: choices}, opts...) +// Acknowledge acknowledges the interaction. +// +// This is used strictly for acknowledging the HTTP interaction request from discord. This responds with 202 Accepted. +// +// When using this, your first http request must be [rest.Interactions.CreateInteractionResponse] or [rest.Interactions.CreateInteractionResponseWithCallback] +// +// This does not produce a visible loading state to the user. +// You are expected to send a new http request within 3 seconds to respond to the interaction. +// This allows you to gracefully handle errors with your sent response & access the resulting message. +// +// If you want to create a visible loading state, use DeferCreateMessage. +// +// Source docs: [Discord Source docs] +// +// [Discord Source docs]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback +func (e *AutocompleteInteractionCreate) Acknowledge(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeAcknowledge, nil, opts...) +} + +// AutocompleteResult responds to the interaction with a slice of choices. +func (e *AutocompleteInteractionCreate) AutocompleteResult(choices []discord.AutocompleteChoice, opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeAutocompleteResult, discord.AutocompleteResult{Choices: choices}, opts...) } // ModalSubmitInteractionCreate indicates that a new modal submit interaction has been created. @@ -146,6 +225,25 @@ func (e *ModalSubmitInteractionCreate) Guild() (discord.Guild, bool) { return discord.Guild{}, false } +// Acknowledge acknowledges the interaction. +// +// This is used strictly for acknowledging the HTTP interaction request from discord. This responds with 202 Accepted. +// +// When using this, your first http request must be [rest.Interactions.CreateInteractionResponse] or [rest.Interactions.CreateInteractionResponseWithCallback] +// +// This does not produce a visible loading state to the user. +// You are expected to send a new http request within 3 seconds to respond to the interaction. +// This allows you to gracefully handle errors with your sent response & access the resulting message. +// +// If you want to create a visible loading state, use DeferCreateMessage. +// +// Source docs: [Discord Source docs] +// +// [Discord Source docs]: https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-callback +func (e *ModalSubmitInteractionCreate) Acknowledge(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeAcknowledge, nil, opts...) +} + // CreateMessage responds to the interaction with a new message. func (e *ModalSubmitInteractionCreate) CreateMessage(messageCreate discord.MessageCreate, opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypeCreateMessage, messageCreate, opts...) @@ -169,3 +267,14 @@ func (e *ModalSubmitInteractionCreate) UpdateMessage(messageUpdate discord.Messa func (e *ModalSubmitInteractionCreate) DeferUpdateMessage(opts ...rest.RequestOpt) error { return e.Respond(discord.InteractionResponseTypeDeferredUpdateMessage, nil, opts...) } + +// Deprecated: Respond with a discord.ButtonStylePremium button instead. +// PremiumRequired responds to the interaction with an upgrade button if available. +func (e *ModalSubmitInteractionCreate) PremiumRequired(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) +} + +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *ModalSubmitInteractionCreate) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} diff --git a/events/listener_adapter.go b/events/listener_adapter.go index ca82951ef..8f68fb77e 100644 --- a/events/listener_adapter.go +++ b/events/listener_adapter.go @@ -1,6 +1,9 @@ package events import ( + "fmt" + "log/slog" + "github.com/disgoorg/disgo/bot" ) @@ -61,6 +64,16 @@ type ListenerAdapter struct { OnEmojiUpdate func(event *EmojiUpdate) OnEmojiDelete func(event *EmojiDelete) + // Entitlement Events + OnEntitlementCreate func(event *EntitlementCreate) + OnEntitlementUpdate func(event *EntitlementUpdate) + OnEntitlementDelete func(event *EntitlementDelete) + + // Subscription Events + OnSubscriptionCreate func(event *SubscriptionCreate) + OnSubscriptionUpdate func(event *SubscriptionUpdate) + OnSubscriptionDelete func(event *SubscriptionDelete) + // Sticker Events OnStickersUpdate func(event *StickersUpdate) OnStickerCreate func(event *StickerCreate) @@ -103,12 +116,20 @@ type ListenerAdapter struct { OnGuildMessageReactionRemoveEmoji func(event *GuildMessageReactionRemoveEmoji) OnGuildMessageReactionRemoveAll func(event *GuildMessageReactionRemoveAll) + // Guild Soundboard Events + OnGuildSoundboardSoundCreate func(event *GuildSoundboardSoundCreate) + OnGuildSoundboardSoundUpdate func(event *GuildSoundboardSoundUpdate) + OnGuildSoundboardSoundDelete func(event *GuildSoundboardSoundDelete) + OnGuildSoundboardSoundsUpdate func(event *GuildSoundboardSoundsUpdate) + OnSoundboardSounds func(event *SoundboardSounds) + // Guild Voice Events - OnVoiceServerUpdate func(event *VoiceServerUpdate) - OnGuildVoiceStateUpdate func(event *GuildVoiceStateUpdate) - OnGuildVoiceJoin func(event *GuildVoiceJoin) - OnGuildVoiceMove func(event *GuildVoiceMove) - OnGuildVoiceLeave func(event *GuildVoiceLeave) + OnVoiceServerUpdate func(event *VoiceServerUpdate) + OnGuildVoiceChannelEffectSend func(event *GuildVoiceChannelEffectSend) + OnGuildVoiceStateUpdate func(event *GuildVoiceStateUpdate) + OnGuildVoiceJoin func(event *GuildVoiceJoin) + OnGuildVoiceMove func(event *GuildVoiceMove) + OnGuildVoiceLeave func(event *GuildVoiceLeave) // Guild StageInstance Events OnStageInstanceCreate func(event *StageInstanceCreate) @@ -139,6 +160,18 @@ type ListenerAdapter struct { OnMessageUpdate func(event *MessageUpdate) OnMessageDelete func(event *MessageDelete) + // Message Poll Events + OnMessagePollVoteAdd func(event *MessagePollVoteAdd) + OnMessagePollVoteRemove func(event *MessagePollVoteRemove) + + // DM Message Poll Events + OnDMMessagePollVoteAdd func(event *DMMessagePollVoteAdd) + OnDMMessagePollVoteRemove func(event *DMMessagePollVoteRemove) + + // Guild Message Poll Events + OnGuildMessagePollVoteAdd func(event *GuildMessagePollVoteAdd) + OnGuildMessagePollVoteRemove func(event *GuildMessagePollVoteRemove) + // Message Reaction Events OnMessageReactionAdd func(event *MessageReactionAdd) OnMessageReactionRemove func(event *MessageReactionRemove) @@ -318,6 +351,34 @@ func (l *ListenerAdapter) OnEvent(event bot.Event) { listener(e) } + // Entitlement Events + case *EntitlementCreate: + if listener := l.OnEntitlementCreate; listener != nil { + listener(e) + } + case *EntitlementUpdate: + if listener := l.OnEntitlementUpdate; listener != nil { + listener(e) + } + case *EntitlementDelete: + if listener := l.OnEntitlementDelete; listener != nil { + listener(e) + } + + // Subscription Events + case *SubscriptionCreate: + if listener := l.OnSubscriptionCreate; listener != nil { + listener(e) + } + case *SubscriptionUpdate: + if listener := l.OnSubscriptionUpdate; listener != nil { + listener(e) + } + case *SubscriptionDelete: + if listener := l.OnSubscriptionDelete; listener != nil { + listener(e) + } + // Sticker Events case *StickersUpdate: if listener := l.OnStickersUpdate; listener != nil { @@ -444,11 +505,37 @@ func (l *ListenerAdapter) OnEvent(event bot.Event) { listener(e) } + // Guild Soundboard Sound Events + case *GuildSoundboardSoundCreate: + if listener := l.OnGuildSoundboardSoundCreate; listener != nil { + listener(e) + } + case *GuildSoundboardSoundUpdate: + if listener := l.OnGuildSoundboardSoundUpdate; listener != nil { + listener(e) + } + case *GuildSoundboardSoundDelete: + if listener := l.OnGuildSoundboardSoundDelete; listener != nil { + listener(e) + } + case *GuildSoundboardSoundsUpdate: + if listener := l.OnGuildSoundboardSoundsUpdate; listener != nil { + listener(e) + } + case *SoundboardSounds: + if listener := l.OnSoundboardSounds; listener != nil { + listener(e) + } + // Guild Voice Events case *VoiceServerUpdate: if listener := l.OnVoiceServerUpdate; listener != nil { listener(e) } + case *GuildVoiceChannelEffectSend: + if listener := l.OnGuildVoiceChannelEffectSend; listener != nil { + listener(e) + } case *GuildVoiceStateUpdate: if listener := l.OnGuildVoiceStateUpdate; listener != nil { listener(e) @@ -552,6 +639,32 @@ func (l *ListenerAdapter) OnEvent(event bot.Event) { listener(e) } + // Message Poll Events + case *MessagePollVoteAdd: + if listener := l.OnMessagePollVoteAdd; listener != nil { + listener(e) + } + case *MessagePollVoteRemove: + if listener := l.OnMessagePollVoteRemove; listener != nil { + listener(e) + } + case *DMMessagePollVoteAdd: + if listener := l.OnDMMessagePollVoteAdd; listener != nil { + listener(e) + } + case *DMMessagePollVoteRemove: + if listener := l.OnDMMessagePollVoteRemove; listener != nil { + listener(e) + } + case *GuildMessagePollVoteAdd: + if listener := l.OnGuildMessagePollVoteAdd; listener != nil { + listener(e) + } + case *GuildMessagePollVoteRemove: + if listener := l.OnGuildMessagePollVoteRemove; listener != nil { + listener(e) + } + // Message Reaction Events case *MessageReactionAdd: if listener := l.OnMessageReactionAdd; listener != nil { @@ -647,6 +760,6 @@ func (l *ListenerAdapter) OnEvent(event bot.Event) { } default: - e.Client().Logger().Errorf("unexpected event received: '%T', event: '%+v'", event, event) + e.Client().Logger().Error("unexpected event received", slog.String("type", fmt.Sprintf("%T", event)), slog.String("data", fmt.Sprintf("%+v", event))) } } diff --git a/events/message_poll_events.go b/events/message_poll_events.go new file mode 100644 index 000000000..e6dd72757 --- /dev/null +++ b/events/message_poll_events.go @@ -0,0 +1,34 @@ +package events + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" +) + +// GenericMessagePollVote is a generic poll vote event (requires gateway.IntentGuildMessagePolls and/or gateway.IntentDirectMessagePolls) +type GenericMessagePollVote struct { + *GenericEvent + UserID snowflake.ID + ChannelID snowflake.ID + MessageID snowflake.ID + GuildID *snowflake.ID + AnswerID int +} + +// Guild returns the discord.Guild where the GenericMessagePollVote happened or empty if it happened in DMs +func (e *GenericMessagePollVote) Guild() (discord.Guild, bool) { + if e.GuildID == nil { + return discord.Guild{}, false + } + return e.Client().Caches().Guild(*e.GuildID) +} + +// MessagePollVoteAdd indicates that a discord.User voted on a discord.Poll (requires gateway.IntentGuildMessagePolls and/or gateway.IntentDirectMessagePolls) +type MessagePollVoteAdd struct { + *GenericMessagePollVote +} + +// MessagePollVoteRemove indicates that a discord.User removed their vote on a discord.Poll (requires gateway.IntentGuildMessagePolls and/or gateway.IntentDirectMessagePolls) +type MessagePollVoteRemove struct { + *GenericMessagePollVote +} diff --git a/events/subscription_events.go b/events/subscription_events.go new file mode 100644 index 000000000..884fb90c0 --- /dev/null +++ b/events/subscription_events.go @@ -0,0 +1,20 @@ +package events + +import "github.com/disgoorg/disgo/discord" + +type GenericSubscriptionEvent struct { + *GenericEvent + discord.Subscription +} + +type SubscriptionCreate struct { + *GenericSubscriptionEvent +} + +type SubscriptionUpdate struct { + *GenericSubscriptionEvent +} + +type SubscriptionDelete struct { + *GenericSubscriptionEvent +} diff --git a/gateway/gateway.go b/gateway/gateway.go index 47d8835e6..a9d748403 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -21,6 +21,30 @@ func (s Status) IsConnected() bool { } } +// String returns the string representation of the Status. +func (s Status) String() string { + switch s { + case StatusUnconnected: + return "Unconnected" + case StatusConnecting: + return "Connecting" + case StatusWaitingForHello: + return "WaitingForHello" + case StatusIdentifying: + return "Identifying" + case StatusResuming: + return "Resuming" + case StatusWaitingForReady: + return "WaitingForReady" + case StatusReady: + return "Ready" + case StatusDisconnected: + return "Disconnected" + default: + return "Unknown" + } +} + // Indicates how far along the client is too connecting. const ( // StatusUnconnected is the initial state when a new Gateway is created. diff --git a/gateway/gateway_config.go b/gateway/gateway_config.go index bb02be4d1..6177652db 100644 --- a/gateway/gateway_config.go +++ b/gateway/gateway_config.go @@ -1,14 +1,15 @@ package gateway import ( - "github.com/disgoorg/log" + "log/slog" + "github.com/gorilla/websocket" ) // DefaultConfig returns a Config with sensible defaults. func DefaultConfig() *Config { return &Config{ - Logger: log.Default(), + Logger: slog.Default(), Dialer: websocket.DefaultDialer, LargeThreshold: 50, Intents: IntentsDefault, @@ -23,8 +24,8 @@ func DefaultConfig() *Config { // Config lets you configure your Gateway instance. type Config struct { - // Logger is the logger of the Gateway. Defaults to log.Default(). - Logger log.Logger + // Logger is the Logger of the Gateway. Defaults to slog.Default(). + Logger *slog.Logger // Dialer is the websocket.Dialer of the Gateway. Defaults to websocket.DefaultDialer. Dialer *websocket.Dialer // LargeThreshold is the threshold for the Gateway. Defaults to 50 @@ -54,8 +55,8 @@ type Config struct { EnableResumeURL bool // RateLimiter is the RateLimiter of the Gateway. Defaults to NewRateLimiter(). RateLimiter RateLimiter - // RateRateLimiterConfigOpts is the RateLimiterConfigOpts of the Gateway. Defaults to nil. - RateRateLimiterConfigOpts []RateLimiterConfigOpt + // RateLimiterConfigOpts is the RateLimiterConfigOpts of the Gateway. Defaults to nil. + RateLimiterConfigOpts []RateLimiterConfigOpt // Presence is the presence it should send on login. Defaults to nil. Presence *MessageDataPresenceUpdate // OS is the OS it should send on login. Defaults to runtime.GOOS. @@ -75,12 +76,12 @@ func (c *Config) Apply(opts []ConfigOpt) { opt(c) } if c.RateLimiter == nil { - c.RateLimiter = NewRateLimiter(c.RateRateLimiterConfigOpts...) + c.RateLimiter = NewRateLimiter(c.RateLimiterConfigOpts...) } } // WithLogger sets the Logger for the Gateway. -func WithLogger(logger log.Logger) ConfigOpt { +func WithLogger(logger *slog.Logger) ConfigOpt { return func(config *Config) { config.Logger = logger } @@ -184,10 +185,10 @@ func WithRateLimiter(rateLimiter RateLimiter) ConfigOpt { } } -// WithRateRateLimiterConfigOpts lets you configure the default RateLimiter. -func WithRateRateLimiterConfigOpts(opts ...RateLimiterConfigOpt) ConfigOpt { +// WithRateLimiterConfigOpts lets you configure the default RateLimiter. +func WithRateLimiterConfigOpts(opts ...RateLimiterConfigOpt) ConfigOpt { return func(config *Config) { - config.RateRateLimiterConfigOpts = append(config.RateRateLimiterConfigOpts, opts...) + config.RateLimiterConfigOpts = append(config.RateLimiterConfigOpts, opts...) } } diff --git a/gateway/gateway_event_type.go b/gateway/gateway_event_type.go index 88ebd49c4..ac4843a13 100644 --- a/gateway/gateway_event_type.go +++ b/gateway/gateway_event_type.go @@ -19,6 +19,9 @@ const ( EventTypeChannelUpdate EventType = "CHANNEL_UPDATE" EventTypeChannelDelete EventType = "CHANNEL_DELETE" EventTypeChannelPinsUpdate EventType = "CHANNEL_PINS_UPDATE" + EventTypeEntitlementCreate EventType = "ENTITLEMENT_CREATE" + EventTypeEntitlementUpdate EventType = "ENTITLEMENT_UPDATE" + EventTypeEntitlementDelete EventType = "ENTITLEMENT_DELETE" EventTypeThreadCreate EventType = "THREAD_CREATE" EventTypeThreadUpdate EventType = "THREAD_UPDATE" EventTypeThreadDelete EventType = "THREAD_DELETE" @@ -46,6 +49,10 @@ const ( EventTypeGuildScheduledEventDelete EventType = "GUILD_SCHEDULED_EVENT_DELETE" EventTypeGuildScheduledEventUserAdd EventType = "GUILD_SCHEDULED_EVENT_USER_ADD" EventTypeGuildScheduledEventUserRemove EventType = "GUILD_SCHEDULED_EVENT_USER_REMOVE" + EventTypeGuildSoundboardSoundCreate EventType = "GUILD_SOUNDBOARD_SOUND_CREATE" + EventTypeGuildSoundboardSoundUpdate EventType = "GUILD_SOUNDBOARD_SOUND_UPDATE" + EventTypeGuildSoundboardSoundDelete EventType = "GUILD_SOUNDBOARD_SOUND_DELETE" + EventTypeGuildSoundboardSoundsUpdate EventType = "GUILD_SOUNDBOARD_SOUNDS_UPDATE" EventTypeIntegrationCreate EventType = "INTEGRATION_CREATE" EventTypeIntegrationUpdate EventType = "INTEGRATION_UPDATE" EventTypeIntegrationDelete EventType = "INTEGRATION_DELETE" @@ -56,16 +63,23 @@ const ( EventTypeMessageUpdate EventType = "MESSAGE_UPDATE" EventTypeMessageDelete EventType = "MESSAGE_DELETE" EventTypeMessageDeleteBulk EventType = "MESSAGE_DELETE_BULK" + EventTypeMessagePollVoteAdd EventType = "MESSAGE_POLL_VOTE_ADD" + EventTypeMessagePollVoteRemove EventType = "MESSAGE_POLL_VOTE_REMOVE" EventTypeMessageReactionAdd EventType = "MESSAGE_REACTION_ADD" EventTypeMessageReactionRemove EventType = "MESSAGE_REACTION_REMOVE" EventTypeMessageReactionRemoveAll EventType = "MESSAGE_REACTION_REMOVE_ALL" EventTypeMessageReactionRemoveEmoji EventType = "MESSAGE_REACTION_REMOVE_EMOJI" EventTypePresenceUpdate EventType = "PRESENCE_UPDATE" + EventTypeSoundboardSounds EventType = "SOUNDBOARD_SOUNDS" EventTypeStageInstanceCreate EventType = "STAGE_INSTANCE_CREATE" EventTypeStageInstanceDelete EventType = "STAGE_INSTANCE_DELETE" EventTypeStageInstanceUpdate EventType = "STAGE_INSTANCE_UPDATE" + EventTypeSubscriptionCreate EventType = "SUBSCRIPTION_CREATE" + EventTypeSubscriptionUpdate EventType = "SUBSCRIPTION_UPDATE" + EventTypeSubscriptionDelete EventType = "SUBSCRIPTION_DELETE" EventTypeTypingStart EventType = "TYPING_START" EventTypeUserUpdate EventType = "USER_UPDATE" + EventTypeVoiceChannelEffectSend EventType = "VOICE_CHANNEL_EFFECT_SEND" EventTypeVoiceStateUpdate EventType = "VOICE_STATE_UPDATE" EventTypeVoiceServerUpdate EventType = "VOICE_SERVER_UPDATE" EventTypeWebhooksUpdate EventType = "WEBHOOKS_UPDATE" diff --git a/gateway/gateway_events.go b/gateway/gateway_events.go index 34a85a6b9..2dbc83e57 100644 --- a/gateway/gateway_events.go +++ b/gateway/gateway_events.go @@ -101,6 +101,48 @@ func (EventChannelDelete) eventData() {} type EventThreadCreate struct { discord.GuildThread ThreadMember discord.ThreadMember `json:"thread_member"` + NewlyCreated bool `json:"newly_created"` +} + +func (e *EventThreadCreate) UnmarshalJSON(data []byte) error { + var guildThread discord.GuildThread + if err := json.Unmarshal(data, &guildThread); err != nil { + return err + } + + e.GuildThread = guildThread + + var v struct { + ThreadMember discord.ThreadMember `json:"thread_member"` + NewlyCreated bool `json:"newly_created"` + } + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + e.ThreadMember = v.ThreadMember + e.NewlyCreated = v.NewlyCreated + return nil +} + +func (e EventThreadCreate) MarshalJSON() ([]byte, error) { + data1, err := json.Marshal(e.GuildThread) + if err != nil { + return nil, err + } + + data2, err := json.Marshal(struct { + ThreadMember discord.ThreadMember `json:"thread_member"` + NewlyCreated bool `json:"newly_created"` + }{ + ThreadMember: e.ThreadMember, + NewlyCreated: e.NewlyCreated, + }) + if err != nil { + return nil, err + } + + return json.SimpleMerge(data1, data2) } func (EventThreadCreate) messageData() {} @@ -187,15 +229,16 @@ func (EventGuildAuditLogEntryCreate) messageData() {} func (EventGuildAuditLogEntryCreate) eventData() {} type EventMessageReactionAdd struct { - UserID snowflake.ID `json:"user_id"` - ChannelID snowflake.ID `json:"channel_id"` - MessageID snowflake.ID `json:"message_id"` - GuildID *snowflake.ID `json:"guild_id"` - Member *discord.Member `json:"member"` - Emoji discord.PartialEmoji `json:"emoji"` - MessageAuthorID *snowflake.ID `json:"message_author_id"` - BurstColors []string `json:"burst_colors"` - Burst bool `json:"burst"` + UserID snowflake.ID `json:"user_id"` + ChannelID snowflake.ID `json:"channel_id"` + MessageID snowflake.ID `json:"message_id"` + GuildID *snowflake.ID `json:"guild_id"` + Member *discord.Member `json:"member"` + Emoji discord.PartialEmoji `json:"emoji"` + MessageAuthorID *snowflake.ID `json:"message_author_id"` + BurstColors []string `json:"burst_colors"` + Burst bool `json:"burst"` + Type discord.MessageReactionType `json:"type"` } func (e *EventMessageReactionAdd) UnmarshalJSON(data []byte) error { @@ -215,13 +258,14 @@ func (EventMessageReactionAdd) messageData() {} func (EventMessageReactionAdd) eventData() {} type EventMessageReactionRemove struct { - UserID snowflake.ID `json:"user_id"` - ChannelID snowflake.ID `json:"channel_id"` - MessageID snowflake.ID `json:"message_id"` - GuildID *snowflake.ID `json:"guild_id"` - Emoji discord.PartialEmoji `json:"emoji"` - BurstColors []string `json:"burst_colors"` - Burst bool `json:"burst"` + UserID snowflake.ID `json:"user_id"` + ChannelID snowflake.ID `json:"channel_id"` + MessageID snowflake.ID `json:"message_id"` + GuildID *snowflake.ID `json:"guild_id"` + Emoji discord.PartialEmoji `json:"emoji"` + BurstColors []string `json:"burst_colors"` + Burst bool `json:"burst"` + Type discord.MessageReactionType `json:"type"` } func (EventMessageReactionRemove) messageData() {} @@ -439,6 +483,36 @@ type EventGuildScheduledEventUserRemove struct { func (EventGuildScheduledEventUserRemove) messageData() {} func (EventGuildScheduledEventUserRemove) eventData() {} +type EventGuildSoundboardSoundCreate struct { + discord.SoundboardSound +} + +func (EventGuildSoundboardSoundCreate) messageData() {} +func (EventGuildSoundboardSoundCreate) eventData() {} + +type EventGuildSoundboardSoundUpdate struct { + discord.SoundboardSound +} + +func (EventGuildSoundboardSoundUpdate) messageData() {} +func (EventGuildSoundboardSoundUpdate) eventData() {} + +type EventGuildSoundboardSoundDelete struct { + SoundID snowflake.ID `json:"sound_id"` + GuildID snowflake.ID `json:"guild_id"` +} + +func (EventGuildSoundboardSoundDelete) messageData() {} +func (EventGuildSoundboardSoundDelete) eventData() {} + +type EventGuildSoundboardSoundsUpdate struct { + SoundboardSounds []discord.SoundboardSound `json:"soundboard_sounds"` + GuildID snowflake.ID `json:"guild_id"` +} + +func (EventGuildSoundboardSoundsUpdate) messageData() {} +func (EventGuildSoundboardSoundsUpdate) eventData() {} + type EventInteractionCreate struct { discord.Interaction } @@ -460,7 +534,18 @@ func (EventInteractionCreate) messageData() {} func (EventInteractionCreate) eventData() {} type EventInviteCreate struct { - discord.Invite + ChannelID snowflake.ID `json:"channel_id"` + Code string `json:"code"` + CreatedAt time.Time `json:"created_at"` + GuildID *snowflake.ID `json:"guild_id"` + Inviter *discord.User `json:"inviter"` + MaxAge int `json:"max_age"` + MaxUses int `json:"max_uses"` + TargetType discord.InviteTargetType `json:"target_type"` + TargetUser *discord.User `json:"target_user"` + TargetApplication *discord.PartialApplication `json:"target_application"` + Temporary bool `json:"temporary"` + Uses int `json:"uses"` } func (EventInviteCreate) messageData() {} @@ -507,6 +592,28 @@ type EventMessageDeleteBulk struct { func (EventMessageDeleteBulk) messageData() {} func (EventMessageDeleteBulk) eventData() {} +type EventMessagePollVoteAdd struct { + UserID snowflake.ID `json:"user_id"` + ChannelID snowflake.ID `json:"channel_id"` + MessageID snowflake.ID `json:"message_id"` + GuildID *snowflake.ID `json:"guild_id"` + AnswerID int `json:"answer_id"` +} + +func (EventMessagePollVoteAdd) messageData() {} +func (EventMessagePollVoteAdd) eventData() {} + +type EventMessagePollVoteRemove struct { + UserID snowflake.ID `json:"user_id"` + ChannelID snowflake.ID `json:"channel_id"` + MessageID snowflake.ID `json:"message_id"` + GuildID *snowflake.ID `json:"guild_id"` + AnswerID int `json:"answer_id"` +} + +func (EventMessagePollVoteRemove) messageData() {} +func (EventMessagePollVoteRemove) eventData() {} + type EventPresenceUpdate struct { discord.Presence } @@ -514,6 +621,14 @@ type EventPresenceUpdate struct { func (EventPresenceUpdate) messageData() {} func (EventPresenceUpdate) eventData() {} +type EventSoundboardSounds struct { + SoundboardSounds []discord.SoundboardSound `json:"soundboard_sounds"` + GuildID snowflake.ID `json:"guild_id"` +} + +func (EventSoundboardSounds) messageData() {} +func (EventSoundboardSounds) eventData() {} + type EventStageInstanceCreate struct { discord.StageInstance } @@ -568,6 +683,41 @@ type EventUserUpdate struct { func (EventUserUpdate) messageData() {} func (EventUserUpdate) eventData() {} +type EventVoiceChannelEffectSend struct { + ChannelID snowflake.ID `json:"channel_id"` + GuildID snowflake.ID `json:"guild_id"` + UserID snowflake.ID `json:"user_id"` + Emoji *discord.Emoji `json:"emoji"` + AnimationType *discord.VoiceChannelEffectAnimationType `json:"animation_type,omitempty"` + AnimationID *int `json:"animation_id,omitempty"` + SoundID *int64 `json:"-"` + SoundVolume *float64 `json:"sound_volume,omitempty"` +} + +func (e *EventVoiceChannelEffectSend) UnmarshalJSON(data []byte) error { + type eventVoiceChannelEffectSend EventVoiceChannelEffectSend + var v struct { + SoundID *json.Number `json:"sound_id"` + eventVoiceChannelEffectSend + } + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *e = EventVoiceChannelEffectSend(v.eventVoiceChannelEffectSend) + if v.SoundID == nil { + return nil + } + i, err := v.SoundID.Int64() + if err != nil { + return err + } + e.SoundID = &i + return nil +} + +func (EventVoiceChannelEffectSend) messageData() {} +func (EventVoiceChannelEffectSend) eventData() {} + type EventVoiceStateUpdate struct { discord.VoiceState Member discord.Member `json:"member"` @@ -599,22 +749,41 @@ type EventIntegrationCreate struct { } func (e *EventIntegrationCreate) UnmarshalJSON(data []byte) error { - type integrationCreateEvent EventIntegrationCreate - var v struct { - discord.UnmarshalIntegration - integrationCreateEvent + var integration discord.UnmarshalIntegration + if err := json.Unmarshal(data, &integration); err != nil { + return err } + var v struct { + GuildID snowflake.ID `json:"guild_id"` + } if err := json.Unmarshal(data, &v); err != nil { return err } - *e = EventIntegrationCreate(v.integrationCreateEvent) - - e.Integration = v.UnmarshalIntegration.Integration + e.Integration = integration.Integration + e.GuildID = v.GuildID return nil } +func (e EventIntegrationCreate) MarshalJSON() ([]byte, error) { + data1, err := json.Marshal(e.Integration) + if err != nil { + return nil, err + } + + data2, err := json.Marshal(struct { + GuildID snowflake.ID `json:"guild_id"` + }{ + GuildID: e.GuildID, + }) + if err != nil { + return nil, err + } + + return json.SimpleMerge(data1, data2) +} + func (EventIntegrationCreate) messageData() {} func (EventIntegrationCreate) eventData() {} @@ -624,22 +793,41 @@ type EventIntegrationUpdate struct { } func (e *EventIntegrationUpdate) UnmarshalJSON(data []byte) error { - type integrationUpdateEvent EventIntegrationUpdate - var v struct { - discord.UnmarshalIntegration - integrationUpdateEvent + var integration discord.UnmarshalIntegration + if err := json.Unmarshal(data, &integration); err != nil { + return err } + var v struct { + GuildID snowflake.ID `json:"guild_id"` + } if err := json.Unmarshal(data, &v); err != nil { return err } - *e = EventIntegrationUpdate(v.integrationUpdateEvent) - - e.Integration = v.UnmarshalIntegration.Integration + e.Integration = integration.Integration + e.GuildID = v.GuildID return nil } +func (e EventIntegrationUpdate) MarshalJSON() ([]byte, error) { + data1, err := json.Marshal(e.Integration) + if err != nil { + return nil, err + } + + data2, err := json.Marshal(struct { + GuildID snowflake.ID `json:"guild_id"` + }{ + GuildID: e.GuildID, + }) + if err != nil { + return nil, err + } + + return json.SimpleMerge(data1, data2) +} + func (EventIntegrationUpdate) messageData() {} func (EventIntegrationUpdate) eventData() {} @@ -705,3 +893,45 @@ type EventHeartbeatAck struct { func (EventHeartbeatAck) messageData() {} func (EventHeartbeatAck) eventData() {} + +type EventEntitlementCreate struct { + discord.Entitlement +} + +func (EventEntitlementCreate) messageData() {} +func (EventEntitlementCreate) eventData() {} + +type EventEntitlementUpdate struct { + discord.Entitlement +} + +func (EventEntitlementUpdate) messageData() {} +func (EventEntitlementUpdate) eventData() {} + +type EventEntitlementDelete struct { + discord.Entitlement +} + +func (EventEntitlementDelete) messageData() {} +func (EventEntitlementDelete) eventData() {} + +type EventSubscriptionCreate struct { + discord.Subscription +} + +func (EventSubscriptionCreate) messageData() {} +func (EventSubscriptionCreate) eventData() {} + +type EventSubscriptionUpdate struct { + discord.Subscription +} + +func (EventSubscriptionUpdate) messageData() {} +func (EventSubscriptionUpdate) eventData() {} + +type EventSubscriptionDelete struct { + discord.Subscription +} + +func (EventSubscriptionDelete) messageData() {} +func (EventSubscriptionDelete) eventData() {} diff --git a/gateway/gateway_impl.go b/gateway/gateway_impl.go index d6a0d09a7..cad397e48 100644 --- a/gateway/gateway_impl.go +++ b/gateway/gateway_impl.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "sync" "syscall" @@ -24,6 +25,7 @@ var _ Gateway = (*gatewayImpl)(nil) func New(token string, eventHandlerFunc EventHandlerFunc, closeHandlerFunc CloseHandlerFunc, opts ...ConfigOpt) Gateway { config := DefaultConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "gateway"), slog.Int("shard_id", config.ShardID), slog.Int("shard_count", config.ShardCount)) return &gatewayImpl{ config: *config, @@ -40,10 +42,10 @@ type gatewayImpl struct { closeHandlerFunc CloseHandlerFunc token string - conn *websocket.Conn - connMu sync.Mutex - heartbeatChan chan struct{} - status Status + conn *websocket.Conn + connMu sync.Mutex + heartbeatCancel context.CancelFunc + status Status heartbeatInterval time.Duration lastHeartbeatSent time.Time @@ -70,26 +72,12 @@ func (g *gatewayImpl) Intents() Intents { return g.config.Intents } -func (g *gatewayImpl) formatLogsf(format string, a ...any) string { - if g.config.ShardCount > 1 { - return fmt.Sprintf("[%d/%d] %s", g.config.ShardID, g.config.ShardCount, fmt.Sprintf(format, a...)) - } - return fmt.Sprintf(format, a...) -} - -func (g *gatewayImpl) formatLogs(a ...any) string { - if g.config.ShardCount > 1 { - return fmt.Sprintf("[%d/%d] %s", g.config.ShardID, g.config.ShardCount, fmt.Sprint(a...)) - } - return fmt.Sprint(a...) -} - func (g *gatewayImpl) Open(ctx context.Context) error { return g.reconnectTry(ctx, 0) } func (g *gatewayImpl) open(ctx context.Context) error { - g.config.Logger.Debug(g.formatLogs("opening gateway connection")) + g.config.Logger.Debug("opening gateway connection") g.connMu.Lock() defer g.connMu.Unlock() @@ -106,20 +94,19 @@ func (g *gatewayImpl) open(ctx context.Context) error { g.lastHeartbeatSent = time.Now().UTC() conn, rs, err := g.config.Dialer.DialContext(ctx, gatewayURL, nil) if err != nil { - g.Close(ctx) - body := "empty" + body := "" if rs != nil && rs.Body != nil { defer func() { _ = rs.Body.Close() }() rawBody, bErr := io.ReadAll(rs.Body) if bErr != nil { - g.config.Logger.Error(g.formatLogs("error while reading response body: ", err)) + g.config.Logger.Error("error while reading response body", slog.Any("err", bErr)) } body = string(rawBody) } - g.config.Logger.Error(g.formatLogsf("error connecting to the gateway. url: %s, error: %s, body: %s", gatewayURL, err, body)) + g.config.Logger.Error("error connecting to the gateway", slog.Any("err", err), slog.String("url", gatewayURL), slog.String("body", body)) return err } @@ -144,18 +131,18 @@ func (g *gatewayImpl) Close(ctx context.Context) { } func (g *gatewayImpl) CloseWithCode(ctx context.Context, code int, message string) { - if g.heartbeatChan != nil { - g.config.Logger.Debug(g.formatLogs("closing heartbeat goroutines...")) - g.heartbeatChan <- struct{}{} + if g.heartbeatCancel != nil { + g.config.Logger.Debug("closing heartbeat goroutines...") + g.heartbeatCancel() } g.connMu.Lock() defer g.connMu.Unlock() if g.conn != nil { g.config.RateLimiter.Close(ctx) - g.config.Logger.Debug(g.formatLogsf("closing gateway connection with code: %d, message: %s", code, message)) + g.config.Logger.Debug("closing gateway connection", slog.Int("code", code), slog.String("message", message)) if err := g.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(code, message)); err != nil && !errors.Is(err, websocket.ErrCloseSent) { - g.config.Logger.Debug(g.formatLogs("error writing close code. error: ", err)) + g.config.Logger.Debug("error writing close code", slog.Any("err", err)) } _ = g.conn.Close() g.conn = nil @@ -167,6 +154,7 @@ func (g *gatewayImpl) CloseWithCode(ctx context.Context, code int, message strin g.config.LastSequenceReceived = nil } } + g.status = StatusDisconnected } func (g *gatewayImpl) Status() Status { @@ -198,7 +186,9 @@ func (g *gatewayImpl) send(ctx context.Context, messageType int, data []byte) er } defer g.config.RateLimiter.Unlock() - g.config.Logger.Trace(g.formatLogs("sending gateway command: ", string(data))) + if g.config.Logger.Enabled(ctx, slog.LevelDebug) { + g.config.Logger.Debug("sending gateway command", slog.String("data", string(data))) + } return g.conn.WriteMessage(messageType, data) } @@ -229,7 +219,7 @@ func (g *gatewayImpl) reconnectTry(ctx context.Context, try int) error { if errors.Is(err, discord.ErrGatewayAlreadyConnected) { return err } - g.config.Logger.Error(g.formatLogs("failed to reconnect gateway. error: ", err)) + g.config.Logger.Error("failed to reconnect gateway", slog.Any("err", err)) g.status = StatusDisconnected return g.reconnectTry(ctx, try+1) } @@ -239,21 +229,21 @@ func (g *gatewayImpl) reconnectTry(ctx context.Context, try int) error { func (g *gatewayImpl) reconnect() { err := g.reconnectTry(context.Background(), 0) if err != nil { - g.config.Logger.Error(g.formatLogs("failed to reopen gateway. error: ", err)) + g.config.Logger.Error("failed to reopen gateway", slog.Any("err", err)) } } func (g *gatewayImpl) heartbeat() { - if g.heartbeatChan == nil { - g.heartbeatChan = make(chan struct{}) - } + ctx, cancel := context.WithCancel(context.Background()) + g.heartbeatCancel = cancel + heartbeatTicker := time.NewTicker(g.heartbeatInterval) defer heartbeatTicker.Stop() - defer g.config.Logger.Debug(g.formatLogs("exiting heartbeat goroutine...")) + defer g.config.Logger.Debug("exiting heartbeat goroutine") for { select { - case <-g.heartbeatChan: + case <-ctx.Done(): return case <-heartbeatTicker.C: @@ -263,7 +253,7 @@ func (g *gatewayImpl) heartbeat() { } func (g *gatewayImpl) sendHeartbeat() { - g.config.Logger.Debug(g.formatLogs("sending heartbeat...")) + g.config.Logger.Debug("sending heartbeat") ctx, cancel := context.WithTimeout(context.Background(), g.heartbeatInterval) defer cancel() @@ -271,8 +261,10 @@ func (g *gatewayImpl) sendHeartbeat() { if errors.Is(err, discord.ErrShardNotConnected) || errors.Is(err, syscall.EPIPE) { return } - g.config.Logger.Error(g.formatLogs("failed to send heartbeat. error: ", err)) - g.CloseWithCode(context.TODO(), websocket.CloseServiceRestart, "heartbeat timeout") + g.config.Logger.Error("failed to send heartbeat", slog.Any("err", err)) + closeCtx, closeCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer closeCancel() + g.CloseWithCode(closeCtx, websocket.CloseServiceRestart, "heartbeat timeout") go g.reconnect() return } @@ -281,7 +273,7 @@ func (g *gatewayImpl) sendHeartbeat() { func (g *gatewayImpl) identify() { g.status = StatusIdentifying - g.config.Logger.Debug(g.formatLogs("sending Identify command...")) + g.config.Logger.Debug("sending Identify command") identify := MessageDataIdentify{ Token: g.token, @@ -297,8 +289,10 @@ func (g *gatewayImpl) identify() { Shard: &[2]int{g.ShardID(), g.ShardCount()}, } - if err := g.Send(context.TODO(), OpcodeIdentify, identify); err != nil { - g.config.Logger.Error(g.formatLogs("error sending Identify command err: ", err)) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := g.Send(ctx, OpcodeIdentify, identify); err != nil { + g.config.Logger.Error("error sending Identify command", slog.Any("err", err)) } g.status = StatusWaitingForReady } @@ -310,18 +304,20 @@ func (g *gatewayImpl) resume() { SessionID: *g.config.SessionID, Seq: *g.config.LastSequenceReceived, } + g.config.Logger.Debug("sending Resume command") - g.config.Logger.Debug(g.formatLogs("sending Resume command...")) - if err := g.Send(context.TODO(), OpcodeResume, resume); err != nil { - g.config.Logger.Error(g.formatLogs("error sending resume command err: ", err)) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := g.Send(ctx, OpcodeResume, resume); err != nil { + g.config.Logger.Error("error sending resume command", slog.Any("err", err)) } } func (g *gatewayImpl) listen(conn *websocket.Conn) { - defer g.config.Logger.Debug(g.formatLogs("exiting listen goroutine...")) + defer g.config.Logger.Debug("exiting listen goroutine") loop: for { - mt, data, err := conn.ReadMessage() + mt, r, err := conn.NextReader() if err != nil { g.connMu.Lock() sameConnection := g.conn == conn @@ -343,17 +339,22 @@ loop: g.config.SessionID = nil g.config.ResumeURL = nil } - message := g.formatLogsf("gateway close received, reconnect: %t, code: %d, error: %s", g.config.AutoReconnect && reconnect, closeError.Code, closeError.Text) + msg := "gateway close received" + args := []any{ + slog.Bool("reconnect", reconnect), + slog.Int("code", closeError.Code), + slog.String("error", closeError.Text), + } if reconnect { - g.config.Logger.Debug(message) + g.config.Logger.Debug(msg, args...) } else { - g.config.Logger.Error(message) + g.config.Logger.Error(msg, args...) } } else if errors.Is(err, net.ErrClosed) { // we closed the connection ourselves. Don't try to reconnect here reconnect = false } else { - g.config.Logger.Debug(g.formatLogs("failed to read next message from gateway. error: ", err)) + g.config.Logger.Debug("failed to read next message from gateway", slog.Any("err", err)) } // make sure the connection is properly closed @@ -369,9 +370,9 @@ loop: break loop } - message, err := g.parseMessage(mt, data) + message, err := g.parseMessage(mt, r) if err != nil { - g.config.Logger.Error(g.formatLogs("error while parsing gateway message. error: ", err)) + g.config.Logger.Error("error while parsing gateway message", slog.Any("err", err)) continue } @@ -393,7 +394,7 @@ loop: eventData, ok := message.D.(EventData) if !ok && message.D != nil { - g.config.Logger.Error(g.formatLogsf("invalid message data of type %T received", message.D)) + g.config.Logger.Error("invalid message data received", slog.String("data", fmt.Sprintf("%T", message.D))) continue } @@ -402,11 +403,11 @@ loop: g.config.SessionID = &readyEvent.SessionID g.config.ResumeURL = &readyEvent.ResumeGatewayURL g.status = StatusReady - g.config.Logger.Debug(g.formatLogs("ready message received")) + g.config.Logger.Debug("ready message received") } if unknownEvent, ok := eventData.(EventUnknown); ok { - g.config.Logger.Debug(g.formatLogsf("unknown event received: %s, data: %s", message.T, unknownEvent)) + g.config.Logger.Debug("unknown event received", slog.String("event", string(message.T)), slog.String("data", string(unknownEvent))) continue } @@ -457,31 +458,35 @@ loop: g.lastHeartbeatReceived = newHeartbeat default: - g.config.Logger.Debug(g.formatLogsf("unknown opcode received: %d, data: %s", message.Op, message.D)) + + g.config.Logger.Debug("unknown opcode received", slog.Int("opcode", int(message.Op)), slog.String("data", fmt.Sprintf("%s", message.D))) } } } -func (g *gatewayImpl) parseMessage(mt int, data []byte) (Message, error) { - var finalData []byte +func (g *gatewayImpl) parseMessage(mt int, r io.Reader) (Message, error) { if mt == websocket.BinaryMessage { - g.config.Logger.Trace(g.formatLogs("binary message received. decompressing...")) + g.config.Logger.Debug("binary message received. decompressing") - reader, err := zlib.NewReader(bytes.NewReader(data)) + reader, err := zlib.NewReader(r) if err != nil { return Message{}, fmt.Errorf("failed to decompress zlib: %w", err) } defer reader.Close() - finalData, err = io.ReadAll(reader) + r = reader + } + + if g.config.Logger.Enabled(context.Background(), slog.LevelDebug) { + buff := new(bytes.Buffer) + tr := io.TeeReader(r, buff) + data, err := io.ReadAll(tr) if err != nil { - return Message{}, fmt.Errorf("failed to read decompressed data: %w", err) + return Message{}, fmt.Errorf("failed to read message: %w", err) } - } else { - finalData = data + g.config.Logger.Debug("received gateway message", slog.String("data", string(data))) + r = buff } - g.config.Logger.Trace(g.formatLogs("received gateway message: ", string(finalData))) - var message Message - return message, json.Unmarshal(finalData, &message) + return message, json.NewDecoder(r).Decode(&message) } diff --git a/gateway/gateway_intents.go b/gateway/gateway_intents.go index f9033c80e..2945dd691 100644 --- a/gateway/gateway_intents.go +++ b/gateway/gateway_intents.go @@ -10,6 +10,7 @@ const ( IntentGuilds Intents = 1 << iota IntentGuildMembers IntentGuildModeration + // Deprecated: Use IntentGuildExpressions instead IntentGuildEmojisAndStickers IntentGuildIntegrations IntentGuildWebhooks @@ -29,11 +30,17 @@ const ( _ IntentAutoModerationConfiguration IntentAutoModerationExecution + _ + _ + IntentGuildMessagePolls + IntentDirectMessagePolls + + IntentGuildExpressions = IntentGuildEmojisAndStickers IntentsGuild = IntentGuilds | IntentGuildMembers | IntentGuildModeration | - IntentGuildEmojisAndStickers | + IntentGuildExpressions | IntentGuildIntegrations | IntentGuildWebhooks | IntentGuildInvites | @@ -42,15 +49,20 @@ const ( IntentGuildMessages | IntentGuildMessageReactions | IntentGuildMessageTyping | - IntentGuildScheduledEvents + IntentGuildScheduledEvents | + IntentGuildMessagePolls IntentsDirectMessage = IntentDirectMessages | IntentDirectMessageReactions | - IntentDirectMessageTyping + IntentDirectMessageTyping | + IntentDirectMessagePolls + + IntentsMessagePolls = IntentGuildMessagePolls | + IntentDirectMessagePolls IntentsNonPrivileged = IntentGuilds | IntentGuildModeration | - IntentGuildEmojisAndStickers | + IntentGuildExpressions | IntentGuildIntegrations | IntentGuildWebhooks | IntentGuildInvites | @@ -63,7 +75,9 @@ const ( IntentDirectMessageTyping | IntentGuildScheduledEvents | IntentAutoModerationConfiguration | - IntentAutoModerationExecution + IntentAutoModerationExecution | + IntentGuildMessagePolls | + IntentDirectMessagePolls IntentsPrivileged = IntentGuildMembers | IntentGuildPresences | IntentMessageContent diff --git a/gateway/gateway_messages.go b/gateway/gateway_messages.go index 8eca2e0f4..26dba4a50 100644 --- a/gateway/gateway_messages.go +++ b/gateway/gateway_messages.go @@ -1,6 +1,8 @@ package gateway import ( + "fmt" + "github.com/disgoorg/json" "github.com/disgoorg/snowflake/v2" @@ -80,13 +82,18 @@ func (e *Message) UnmarshalJSON(data []byte) error { case OpcodeHeartbeatACK: + case OpcodeRequestSoundboardSounds: + var d MessageDataRequestSoundboardSounds + err = json.Unmarshal(v.D, &d) + messageData = d + default: var d MessageDataUnknown err = json.Unmarshal(v.D, &d) messageData = d } if err != nil { - return err + return fmt.Errorf("failed to unmarshal message data: %s: %w", string(data), err) } e.Op = v.Op e.S = v.S @@ -159,6 +166,21 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { err = json.Unmarshal(data, &d) eventData = d + case EventTypeEntitlementCreate: + var d EventEntitlementCreate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeEntitlementUpdate: + var d EventEntitlementUpdate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeEntitlementDelete: + var d EventEntitlementDelete + err = json.Unmarshal(data, &d) + eventData = d + case EventTypeThreadCreate: var d EventThreadCreate err = json.Unmarshal(data, &d) @@ -294,6 +316,26 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { err = json.Unmarshal(data, &d) eventData = d + case EventTypeGuildSoundboardSoundCreate: + var d EventGuildSoundboardSoundCreate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeGuildSoundboardSoundUpdate: + var d EventGuildSoundboardSoundUpdate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeGuildSoundboardSoundDelete: + var d EventGuildSoundboardSoundDelete + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeGuildSoundboardSoundsUpdate: + var d EventGuildSoundboardSoundsUpdate + err = json.Unmarshal(data, &d) + eventData = d + case EventTypeIntegrationCreate: var d EventIntegrationCreate err = json.Unmarshal(data, &d) @@ -369,6 +411,11 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { err = json.Unmarshal(data, &d) eventData = d + case EventTypeSoundboardSounds: + var d EventSoundboardSounds + err = json.Unmarshal(data, &d) + eventData = d + case EventTypeStageInstanceCreate: var d EventStageInstanceCreate err = json.Unmarshal(data, &d) @@ -384,6 +431,21 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { err = json.Unmarshal(data, &d) eventData = d + case EventTypeSubscriptionCreate: + var d EventSubscriptionCreate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeSubscriptionUpdate: + var d EventSubscriptionUpdate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeSubscriptionDelete: + var d EventSubscriptionDelete + err = json.Unmarshal(data, &d) + eventData = d + case EventTypeTypingStart: var d EventTypingStart err = json.Unmarshal(data, &d) @@ -394,6 +456,11 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { err = json.Unmarshal(data, &d) eventData = d + case EventTypeVoiceChannelEffectSend: + var d EventVoiceChannelEffectSend + err = json.Unmarshal(data, &d) + eventData = d + case EventTypeVoiceStateUpdate: var d EventVoiceStateUpdate err = json.Unmarshal(data, &d) @@ -415,7 +482,11 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { eventData = d } - return eventData, err + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event data: %s: %w", string(data), err) + } + + return eventData, nil } type MessageDataUnknown json.RawMessage @@ -595,3 +666,9 @@ type MessageDataHello struct { } func (MessageDataHello) messageData() {} + +type MessageDataRequestSoundboardSounds struct { + GuildIDs []snowflake.ID `json:"guild_ids"` +} + +func (MessageDataRequestSoundboardSounds) messageData() {} diff --git a/gateway/gateway_opcodes.go b/gateway/gateway_opcodes.go index 85413d550..d6c8e045d 100644 --- a/gateway/gateway_opcodes.go +++ b/gateway/gateway_opcodes.go @@ -17,6 +17,7 @@ const ( OpcodeInvalidSession OpcodeHello OpcodeHeartbeatACK + OpcodeRequestSoundboardSounds Opcode = 31 ) type CloseEventCode struct { diff --git a/gateway/gateway_rate_limiter.go b/gateway/gateway_rate_limiter.go index 027ec7b0e..b8349ef52 100644 --- a/gateway/gateway_rate_limiter.go +++ b/gateway/gateway_rate_limiter.go @@ -4,6 +4,9 @@ import ( "context" ) +// CommandsPerMinute is the default number of commands per minute that the Gateway will allow. +const CommandsPerMinute = 120 + // RateLimiter provides handles the rate limiting logic for connecting to Discord's Gateway. type RateLimiter interface { // Close gracefully closes the RateLimiter. diff --git a/gateway/gateway_rate_limiter_config.go b/gateway/gateway_rate_limiter_config.go index 0f2afa005..fa5ff8d9c 100644 --- a/gateway/gateway_rate_limiter_config.go +++ b/gateway/gateway_rate_limiter_config.go @@ -1,20 +1,20 @@ package gateway import ( - "github.com/disgoorg/log" + "log/slog" ) // DefaultRateLimiterConfig returns a RateLimiterConfig with sensible defaults. func DefaultRateLimiterConfig() *RateLimiterConfig { return &RateLimiterConfig{ - Logger: log.Default(), - CommandsPerMinute: 120, + Logger: slog.Default(), + CommandsPerMinute: CommandsPerMinute, } } // RateLimiterConfig lets you configure your Gateway instance. type RateLimiterConfig struct { - Logger log.Logger + Logger *slog.Logger CommandsPerMinute int } @@ -29,7 +29,7 @@ func (c *RateLimiterConfig) Apply(opts []RateLimiterConfigOpt) { } // WithRateLimiterLogger sets the Logger for the Gateway. -func WithRateLimiterLogger(logger log.Logger) RateLimiterConfigOpt { +func WithRateLimiterLogger(logger *slog.Logger) RateLimiterConfigOpt { return func(config *RateLimiterConfig) { config.Logger = logger } diff --git a/gateway/gateway_rate_limiter_impl.go b/gateway/gateway_rate_limiter_impl.go index 372d70776..defca34f1 100644 --- a/gateway/gateway_rate_limiter_impl.go +++ b/gateway/gateway_rate_limiter_impl.go @@ -2,6 +2,7 @@ package gateway import ( "context" + "log/slog" "time" "github.com/sasha-s/go-csync" @@ -11,6 +12,7 @@ import ( func NewRateLimiter(opts ...RateLimiterConfigOpt) RateLimiter { config := DefaultRateLimiterConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "gateway_rate_limiter")) return &rateLimiterImpl{ config: *config, @@ -37,7 +39,7 @@ func (l *rateLimiterImpl) Reset() { } func (l *rateLimiterImpl) Wait(ctx context.Context) error { - l.config.Logger.Trace("locking gateway rate limiter") + l.config.Logger.Debug("locking gateway rate limiter") if err := l.mu.CLock(ctx); err != nil { return err } @@ -62,7 +64,7 @@ func (l *rateLimiterImpl) Wait(ctx context.Context) error { } func (l *rateLimiterImpl) Unlock() { - l.config.Logger.Trace("unlocking gateway rate limiter") + l.config.Logger.Debug("unlocking gateway rate limiter") now := time.Now() if l.reset.Before(now) { l.reset = now.Add(time.Minute) diff --git a/go.mod b/go.mod index 2517f4d80..9f5df6e8b 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,19 @@ module github.com/disgoorg/disgo -go 1.18 +go 1.21 require ( - github.com/disgoorg/json v1.1.0 - github.com/disgoorg/log v1.2.1 - github.com/disgoorg/snowflake/v2 v2.0.1 - github.com/gorilla/websocket v1.5.0 - github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b - github.com/stretchr/testify v1.8.1 - golang.org/x/crypto v0.12.0 - golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b + github.com/disgoorg/json v1.2.0 + github.com/disgoorg/snowflake/v2 v2.0.3 + github.com/gorilla/websocket v1.5.3 + github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad + github.com/stretchr/testify v1.10.0 + golang.org/x/crypto v0.31.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/sys v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5f0f4570e..a3c3cc2d1 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,22 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/disgoorg/json v1.1.0 h1:7xigHvomlVA9PQw9bMGO02PHGJJPqvX5AnwlYg/Tnys= -github.com/disgoorg/json v1.1.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= -github.com/disgoorg/log v1.2.1 h1:kZYAWkUBcGy4LbZcgYtgYu49xNVLy+xG5Uq3yz5VVQs= -github.com/disgoorg/log v1.2.1/go.mod h1:hhQWYTFTnIGzAuFPZyXJEi11IBm9wq+/TVZt/FEwX0o= -github.com/disgoorg/snowflake/v2 v2.0.1 h1:CuUxGLwggUxEswZOmZ+mZ5i0xSumQdXW9tXW7uGqe+0= -github.com/disgoorg/snowflake/v2 v2.0.1/go.mod h1:SPU9c2CNn5DSyb86QcKtdZgix9osEtKrHLW4rMhfLCs= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/disgoorg/json v1.2.0 h1:6e/j4BCfSHIvucG1cd7tJPAOp1RgnnMFSqkvZUtEd1Y= +github.com/disgoorg/json v1.2.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= +github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= +github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b h1:qYTY2tN72LhgDj2rtWG+LI6TXFl2ygFQQ4YezfVaGQE= -github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI= -golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler/autocomplete.go b/handler/autocomplete.go index fafb2e78a..c210b4618 100644 --- a/handler/autocomplete.go +++ b/handler/autocomplete.go @@ -1,6 +1,8 @@ package handler import ( + "context" + "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" @@ -10,7 +12,8 @@ import ( type AutocompleteEvent struct { *events.AutocompleteInteractionCreate - Variables map[string]string + Vars map[string]string + Ctx context.Context } func (e *AutocompleteEvent) GetFollowupMessage(messageID snowflake.ID, opts ...rest.RequestOpt) (*discord.Message, error) { diff --git a/handler/command.go b/handler/command.go index 7e6cfc0e4..b399b8178 100644 --- a/handler/command.go +++ b/handler/command.go @@ -1,6 +1,8 @@ package handler import ( + "context" + "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" @@ -10,7 +12,8 @@ import ( type CommandEvent struct { *events.ApplicationCommandInteractionCreate - Variables map[string]string + Vars map[string]string + Ctx context.Context } func (e *CommandEvent) GetInteractionResponse(opts ...rest.RequestOpt) (*discord.Message, error) { diff --git a/handler/component.go b/handler/component.go index 1d7f2d06b..9502a1287 100644 --- a/handler/component.go +++ b/handler/component.go @@ -1,6 +1,8 @@ package handler import ( + "context" + "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" @@ -10,7 +12,8 @@ import ( type ComponentEvent struct { *events.ComponentInteractionCreate - Variables map[string]string + Vars map[string]string + Ctx context.Context } func (e *ComponentEvent) GetInteractionResponse(opts ...rest.RequestOpt) (*discord.Message, error) { diff --git a/handler/handler.go b/handler/handler.go index 63d60b805..b01c65eb8 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -5,7 +5,7 @@ // Slash Commands can have subcommands, which are nested paths. For example /test/subcommand1 or /test/subcommandgroup/subcommand. // // The handler also supports variables in its path which is especially useful for subcommands, components and modals. -// Variables are defined by curly braces like {variable} and can be accessed in the handler via the Variables map. +// Vars are defined by curly braces like {variable} and can be accessed in the handler via the Vars map. // // You can also register middlewares, which are executed before the handler is called. Middlewares can be used to check permissions, validate input or do other things. // Middlewares can also be attached to sub-routers, which is useful if you want to have a middleware for all subcommands of a command as an example. @@ -19,6 +19,7 @@ package handler import ( "errors" + "slices" "strings" "github.com/disgoorg/snowflake/v2" @@ -48,10 +49,11 @@ type handlerHolder[T any] struct { pattern string handler T t discord.InteractionType + t2 []int } -func (h *handlerHolder[T]) Match(path string, t discord.InteractionType) bool { - if h.t != t { +func (h *handlerHolder[T]) Match(path string, t discord.InteractionType, t2 int) bool { + if h.t != t || (len(h.t2) > 0 && !slices.Contains(h.t2, t2)) { return false } parts := splitPath(path) @@ -69,10 +71,12 @@ func (h *handlerHolder[T]) Match(path string, t discord.InteractionType) bool { return true } -func (h *handlerHolder[T]) Handle(path string, variables map[string]string, event *events.InteractionCreate) error { - parseVariables(path, h.pattern, variables) +func (h *handlerHolder[T]) Handle(path string, event *InteractionEvent) error { + parseVariables(path, h.pattern, event.Vars) switch handler := any(h.handler).(type) { + case InteractionHandler: + return handler(event) case CommandHandler: return handler(&CommandEvent{ ApplicationCommandInteractionCreate: &events.ApplicationCommandInteractionCreate{ @@ -80,7 +84,52 @@ func (h *handlerHolder[T]) Handle(path string, variables map[string]string, even ApplicationCommandInteraction: event.Interaction.(discord.ApplicationCommandInteraction), Respond: event.Respond, }, - Variables: variables, + Vars: event.Vars, + Ctx: event.Ctx, + }) + case SlashCommandHandler: + commandInteraction := event.Interaction.(discord.ApplicationCommandInteraction) + return handler(commandInteraction.Data.(discord.SlashCommandInteractionData), &CommandEvent{ + ApplicationCommandInteractionCreate: &events.ApplicationCommandInteractionCreate{ + GenericEvent: event.GenericEvent, + ApplicationCommandInteraction: commandInteraction, + Respond: event.Respond, + }, + Vars: event.Vars, + Ctx: event.Ctx, + }) + case UserCommandHandler: + commandInteraction := event.Interaction.(discord.ApplicationCommandInteraction) + return handler(commandInteraction.Data.(discord.UserCommandInteractionData), &CommandEvent{ + ApplicationCommandInteractionCreate: &events.ApplicationCommandInteractionCreate{ + GenericEvent: event.GenericEvent, + ApplicationCommandInteraction: commandInteraction, + Respond: event.Respond, + }, + Vars: event.Vars, + Ctx: event.Ctx, + }) + case MessageCommandHandler: + commandInteraction := event.Interaction.(discord.ApplicationCommandInteraction) + return handler(commandInteraction.Data.(discord.MessageCommandInteractionData), &CommandEvent{ + ApplicationCommandInteractionCreate: &events.ApplicationCommandInteractionCreate{ + GenericEvent: event.GenericEvent, + ApplicationCommandInteraction: commandInteraction, + Respond: event.Respond, + }, + Vars: event.Vars, + Ctx: event.Ctx, + }) + case EntryPointCommandHandler: + commandInteraction := event.Interaction.(discord.ApplicationCommandInteraction) + return handler(commandInteraction.Data.(discord.EntryPointCommandInteractionData), &CommandEvent{ + ApplicationCommandInteractionCreate: &events.ApplicationCommandInteractionCreate{ + GenericEvent: event.GenericEvent, + ApplicationCommandInteraction: commandInteraction, + Respond: event.Respond, + }, + Vars: event.Vars, + Ctx: event.Ctx, }) case AutocompleteHandler: return handler(&AutocompleteEvent{ @@ -89,7 +138,8 @@ func (h *handlerHolder[T]) Handle(path string, variables map[string]string, even AutocompleteInteraction: event.Interaction.(discord.AutocompleteInteraction), Respond: event.Respond, }, - Variables: variables, + Vars: event.Vars, + Ctx: event.Ctx, }) case ComponentHandler: return handler(&ComponentEvent{ @@ -98,7 +148,30 @@ func (h *handlerHolder[T]) Handle(path string, variables map[string]string, even ComponentInteraction: event.Interaction.(discord.ComponentInteraction), Respond: event.Respond, }, - Variables: variables, + Vars: event.Vars, + Ctx: event.Ctx, + }) + case ButtonComponentHandler: + componentInteraction := event.Interaction.(discord.ComponentInteraction) + return handler(componentInteraction.Data.(discord.ButtonInteractionData), &ComponentEvent{ + ComponentInteractionCreate: &events.ComponentInteractionCreate{ + GenericEvent: event.GenericEvent, + ComponentInteraction: componentInteraction, + Respond: event.Respond, + }, + Vars: event.Vars, + Ctx: event.Ctx, + }) + case SelectMenuComponentHandler: + componentInteraction := event.Interaction.(discord.ComponentInteraction) + return handler(componentInteraction.Data.(discord.SelectMenuInteractionData), &ComponentEvent{ + ComponentInteractionCreate: &events.ComponentInteractionCreate{ + GenericEvent: event.GenericEvent, + ComponentInteraction: componentInteraction, + Respond: event.Respond, + }, + Vars: event.Vars, + Ctx: event.Ctx, }) case ModalHandler: return handler(&ModalEvent{ @@ -107,7 +180,8 @@ func (h *handlerHolder[T]) Handle(path string, variables map[string]string, even ModalSubmitInteraction: event.Interaction.(discord.ModalSubmitInteraction), Respond: event.Respond, }, - Variables: variables, + Vars: event.Vars, + Ctx: event.Ctx, }) } return errors.New("unknown handler type") diff --git a/handler/interaction.go b/handler/interaction.go new file mode 100644 index 000000000..61df07f12 --- /dev/null +++ b/handler/interaction.go @@ -0,0 +1,89 @@ +package handler + +import ( + "context" + + "github.com/disgoorg/snowflake/v2" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/rest" +) + +type InteractionEvent struct { + *events.InteractionCreate + Vars map[string]string + Ctx context.Context +} + +// CreateMessage responds to the interaction with a new message. +func (e *InteractionEvent) CreateMessage(messageCreate discord.MessageCreate, opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeCreateMessage, messageCreate, opts...) +} + +// DeferCreateMessage responds to the interaction with a "bot is thinking..." message which should be edited later. +func (e *InteractionEvent) DeferCreateMessage(ephemeral bool, opts ...rest.RequestOpt) error { + var data discord.InteractionResponseData + if ephemeral { + data = discord.MessageCreate{Flags: discord.MessageFlagEphemeral} + } + return e.Respond(discord.InteractionResponseTypeDeferredCreateMessage, data, opts...) +} + +// UpdateMessage responds to the interaction with updating the message the component is from. +func (e *InteractionEvent) UpdateMessage(messageUpdate discord.MessageUpdate, opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeUpdateMessage, messageUpdate, opts...) +} + +// DeferUpdateMessage responds to the interaction with nothing. +func (e *InteractionEvent) DeferUpdateMessage(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeDeferredUpdateMessage, nil, opts...) +} + +// Deprecated: Respond with a discord.ButtonStylePremium button instead. +// PremiumRequired responds to the interaction with an upgrade button if available. +func (e *InteractionEvent) PremiumRequired(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypePremiumRequired, nil, opts...) +} + +// LaunchActivity responds to the interaction by launching activity associated with the app. +func (e *InteractionEvent) LaunchActivity(opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeLaunchActivity, nil, opts...) +} + +// Modal responds to the interaction with a new modal. +func (e *InteractionEvent) Modal(modalCreate discord.ModalCreate, opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeModal, modalCreate, opts...) +} + +func (e *InteractionEvent) AutocompleteResult(choices []discord.AutocompleteChoice, opts ...rest.RequestOpt) error { + return e.Respond(discord.InteractionResponseTypeAutocompleteResult, discord.AutocompleteResult{Choices: choices}, opts...) +} + +func (e *InteractionEvent) GetInteractionResponse(opts ...rest.RequestOpt) (*discord.Message, error) { + return e.Client().Rest().GetInteractionResponse(e.ApplicationID(), e.Token(), opts...) +} + +func (e *InteractionEvent) UpdateInteractionResponse(messageUpdate discord.MessageUpdate, opts ...rest.RequestOpt) (*discord.Message, error) { + return e.Client().Rest().UpdateInteractionResponse(e.ApplicationID(), e.Token(), messageUpdate, opts...) +} + +func (e *InteractionEvent) DeleteInteractionResponse(opts ...rest.RequestOpt) error { + return e.Client().Rest().DeleteInteractionResponse(e.ApplicationID(), e.Token(), opts...) +} + +func (e *InteractionEvent) GetFollowupMessage(messageID snowflake.ID, opts ...rest.RequestOpt) (*discord.Message, error) { + return e.Client().Rest().GetFollowupMessage(e.ApplicationID(), e.Token(), messageID, opts...) +} + +func (e *InteractionEvent) CreateFollowupMessage(messageCreate discord.MessageCreate, opts ...rest.RequestOpt) (*discord.Message, error) { + return e.Client().Rest().CreateFollowupMessage(e.ApplicationID(), e.Token(), messageCreate, opts...) +} + +func (e *InteractionEvent) UpdateFollowupMessage(messageID snowflake.ID, messageUpdate discord.MessageUpdate, opts ...rest.RequestOpt) (*discord.Message, error) { + return e.Client().Rest().UpdateFollowupMessage(e.ApplicationID(), e.Token(), messageID, messageUpdate, opts...) +} + +func (e *InteractionEvent) DeleteFollowupMessage(messageID snowflake.ID, opts ...rest.RequestOpt) error { + return e.Client().Rest().DeleteFollowupMessage(e.ApplicationID(), e.Token(), messageID, opts...) +} diff --git a/handler/middleware.go b/handler/middleware.go index d38732b95..1c0a93c1b 100644 --- a/handler/middleware.go +++ b/handler/middleware.go @@ -1,12 +1,8 @@ package handler -import ( - "github.com/disgoorg/disgo/events" -) +type Handler func(e *InteractionEvent) error type ( - Handler func(e *events.InteractionCreate) error - Middleware func(next Handler) Handler Middlewares []Middleware diff --git a/handler/middleware/defer.go b/handler/middleware/defer.go index ee5fa4455..975f3983e 100644 --- a/handler/middleware/defer.go +++ b/handler/middleware/defer.go @@ -2,7 +2,6 @@ package middleware import ( "github.com/disgoorg/disgo/discord" - "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/handler" ) @@ -13,7 +12,7 @@ import ( // Note: You can use this middleware in combination with the Go middleware to defer & run in a goroutine. func Defer(interactionType discord.InteractionType, updateMessage bool, ephemeral bool) handler.Middleware { return func(next handler.Handler) handler.Handler { - return func(event *events.InteractionCreate) error { + return func(event *handler.InteractionEvent) error { if event.Type() == interactionType { responseType := discord.InteractionResponseTypeDeferredCreateMessage if updateMessage { diff --git a/handler/middleware/go.go b/handler/middleware/go.go index c67ed1654..aff6cc22f 100644 --- a/handler/middleware/go.go +++ b/handler/middleware/go.go @@ -1,18 +1,41 @@ package middleware import ( - "github.com/disgoorg/disgo/events" + "log/slog" + + "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/handler" ) -// Go is a middleware that runs the next handler in a goroutine -var Go handler.Middleware = func(next handler.Handler) handler.Handler { - return func(e *events.InteractionCreate) error { - go func() { - if err := next(e); err != nil { - e.Client().Logger().Errorf("failed to handle interaction: %s\n", err) - } - }() - return nil +// Go is a middleware that runs the next handler in a goroutine. +var Go = GoErr(func(event *handler.InteractionEvent, err error) { + event.Client().Logger().Error("failed to handle interaction", slog.Any("err", err)) +}) + +// GoDefer combines Go and Defer +func GoDefer(interactionType discord.InteractionType, updateMessage bool, ephemeral bool) handler.Middleware { + return func(next handler.Handler) handler.Handler { + return Go(Defer(interactionType, updateMessage, ephemeral)(next)) + } +} + +// GoErr is a middleware that runs the next handler in a goroutine and lets you handle the error which may occur. +func GoErr(h handler.ErrorHandler) handler.Middleware { + return func(next handler.Handler) handler.Handler { + return func(event *handler.InteractionEvent) error { + go func() { + if err := next(event); err != nil { + h(event, err) + } + }() + return nil + } + } +} + +// GoErrDefer combines GoErr and Defer +func GoErrDefer(h handler.ErrorHandler, interactionType discord.InteractionType, updateMessage bool, ephemeral bool) handler.Middleware { + return func(next handler.Handler) handler.Handler { + return GoErr(h)(Defer(interactionType, updateMessage, ephemeral)(next)) } } diff --git a/handler/middleware/logger.go b/handler/middleware/logger.go index 2e9f1dea9..1fbd0f38a 100644 --- a/handler/middleware/logger.go +++ b/handler/middleware/logger.go @@ -1,13 +1,14 @@ package middleware import ( - "github.com/disgoorg/disgo/events" + "log/slog" + "github.com/disgoorg/disgo/handler" ) var Logger handler.Middleware = func(next handler.Handler) handler.Handler { - return func(e *events.InteractionCreate) error { - e.Client().Logger().Infof("handling interaction: %s\n", e.Interaction.ID()) - return next(e) + return func(event *handler.InteractionEvent) error { + event.Client().Logger().Info("handling interaction", slog.Int64("interaction_id", int64(event.Interaction.ID()))) + return next(event) } } diff --git a/handler/middleware/print.go b/handler/middleware/print.go index f209bc83b..03cd3dbeb 100644 --- a/handler/middleware/print.go +++ b/handler/middleware/print.go @@ -1,13 +1,12 @@ package middleware import ( - "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/handler" ) func Print(content string) handler.Middleware { return func(next handler.Handler) handler.Handler { - return func(event *events.InteractionCreate) error { + return func(event *handler.InteractionEvent) error { println(content) return next(event) } diff --git a/handler/modal.go b/handler/modal.go index cdf0b6568..69240b869 100644 --- a/handler/modal.go +++ b/handler/modal.go @@ -1,6 +1,8 @@ package handler import ( + "context" + "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" @@ -10,7 +12,8 @@ import ( type ModalEvent struct { *events.ModalSubmitInteractionCreate - Variables map[string]string + Vars map[string]string + Ctx context.Context } func (e *ModalEvent) GetInteractionResponse(opts ...rest.RequestOpt) (*discord.Message, error) { diff --git a/handler/mux.go b/handler/mux.go index 5577598f6..7faa89f9d 100644 --- a/handler/mux.go +++ b/handler/mux.go @@ -1,6 +1,8 @@ package handler import ( + "context" + "log/slog" "strings" "github.com/disgoorg/disgo/bot" @@ -8,8 +10,8 @@ import ( "github.com/disgoorg/disgo/events" ) -var defaultErrorHandler = func(e *events.InteractionCreate, err error) { - e.Client().Logger().Errorf("error handling interaction: %v\n", err) +var defaultErrorHandler ErrorHandler = func(event *InteractionEvent, err error) { + event.Client().Logger().Error("error handling interaction", slog.Any("err", err)) } // New returns a new Router. @@ -32,6 +34,7 @@ type Mux struct { routes []Route notFoundHandler NotFoundHandler errorHandler ErrorHandler + defaultContext func() context.Context } // OnEvent is called when a new event is received. @@ -57,17 +60,29 @@ func (r *Mux) OnEvent(event bot.Event) { path = i.Data.CustomID } - if err := r.Handle(path, make(map[string]string), e); err != nil { + var ctx context.Context + if r.defaultContext != nil { + ctx = r.defaultContext() + } else { + ctx = context.Background() + } + + ie := &InteractionEvent{ + InteractionCreate: e, + Ctx: ctx, + Vars: make(map[string]string), + } + if err := r.Handle(path, ie); err != nil { if r.errorHandler != nil { - r.errorHandler(e, err) + r.errorHandler(ie, err) return } - defaultErrorHandler(e, err) + defaultErrorHandler(ie, err) } } // Match returns true if the given path matches the Route. -func (r *Mux) Match(path string, t discord.InteractionType) bool { +func (r *Mux) Match(path string, t discord.InteractionType, t2 int) bool { if r.pattern != "" { parts := splitPath(path) patternParts := splitPath(r.pattern) @@ -84,7 +99,7 @@ func (r *Mux) Match(path string, t discord.InteractionType) bool { } for _, matcher := range r.routes { - if matcher.Match(path, t) { + if matcher.Match(path, t, t2) { return true } } @@ -92,26 +107,35 @@ func (r *Mux) Match(path string, t discord.InteractionType) bool { } // Handle handles the given interaction event. -func (r *Mux) Handle(path string, variables map[string]string, e *events.InteractionCreate) error { - handlerChain := func(event *events.InteractionCreate) error { - path = parseVariables(path, r.pattern, variables) +func (r *Mux) Handle(path string, event *InteractionEvent) error { + path = parseVariables(path, r.pattern, event.Vars) + + handlerChain := Handler(func(event *InteractionEvent) error { + t := event.Type() + var t2 int + switch i := event.Interaction.(type) { + case discord.ApplicationCommandInteraction: + t2 = int(i.Data.Type()) + case discord.ComponentInteraction: + t2 = int(i.Data.Type()) + } for _, route := range r.routes { - if route.Match(path, e.Type()) { - return route.Handle(path, variables, e) + if route.Match(path, t, t2) { + return route.Handle(path, event) } } if r.notFoundHandler != nil { - return r.notFoundHandler(e) + return r.notFoundHandler(event) } return nil - } + }) for i := len(r.middlewares) - 1; i >= 0; i-- { handlerChain = r.middlewares[i](handlerChain) } - return handlerChain(e) + return handlerChain(event) } // Use adds the given middlewares to the current Router. @@ -153,6 +177,17 @@ func (r *Mux) handle(route Route) { r.routes = append(r.routes, route) } +// Interaction registers the given InteractionHandler to the current Router. +// This is a shortcut for Command, Autocomplete, Component and Modal. +func (r *Mux) Interaction(pattern string, h InteractionHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[InteractionHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionType(0), + }) +} + // Command registers the given CommandHandler to the current Router. func (r *Mux) Command(pattern string, h CommandHandler) { checkPattern(pattern) @@ -163,6 +198,50 @@ func (r *Mux) Command(pattern string, h CommandHandler) { }) } +// SlashCommand registers the given SlashCommandHandler to the current Router. +func (r *Mux) SlashCommand(pattern string, h SlashCommandHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[SlashCommandHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionTypeApplicationCommand, + t2: []int{int(discord.ApplicationCommandTypeSlash)}, + }) +} + +// UserCommand registers the given UserCommandHandler to the current Router. +func (r *Mux) UserCommand(pattern string, h UserCommandHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[UserCommandHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionTypeApplicationCommand, + t2: []int{int(discord.ApplicationCommandTypeUser)}, + }) +} + +// MessageCommand registers the given MessageCommandHandler to the current Router. +func (r *Mux) MessageCommand(pattern string, h MessageCommandHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[MessageCommandHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionTypeApplicationCommand, + t2: []int{int(discord.ApplicationCommandTypeMessage)}, + }) +} + +// EntryPointCommand registers the given EntryPointCommandHandler to the current Router. +func (r *Mux) EntryPointCommand(pattern string, h EntryPointCommandHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[EntryPointCommandHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionTypeApplicationCommand, + t2: []int{int(discord.ApplicationCommandTypePrimaryEntryPoint)}, + }) +} + // Autocomplete registers the given AutocompleteHandler to the current Router. func (r *Mux) Autocomplete(pattern string, h AutocompleteHandler) { checkPattern(pattern) @@ -183,6 +262,34 @@ func (r *Mux) Component(pattern string, h ComponentHandler) { }) } +// ButtonComponent registers the given ButtonComponentHandler to the current Router. +func (r *Mux) ButtonComponent(pattern string, h ButtonComponentHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[ButtonComponentHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionTypeComponent, + t2: []int{int(discord.ComponentTypeButton)}, + }) +} + +// SelectMenuComponent registers the given SelectMenuComponentHandler to the current Router. +func (r *Mux) SelectMenuComponent(pattern string, h SelectMenuComponentHandler) { + checkPattern(pattern) + r.handle(&handlerHolder[SelectMenuComponentHandler]{ + pattern: pattern, + handler: h, + t: discord.InteractionTypeComponent, + t2: []int{ + int(discord.ComponentTypeStringSelectMenu), + int(discord.ComponentTypeUserSelectMenu), + int(discord.ComponentTypeRoleSelectMenu), + int(discord.ComponentTypeMentionableSelectMenu), + int(discord.ComponentTypeChannelSelectMenu), + }, + }) +} + // Modal registers the given ModalHandler to the current Router. func (r *Mux) Modal(pattern string, h ModalHandler) { checkPattern(pattern) @@ -205,6 +312,12 @@ func (r *Mux) Error(h ErrorHandler) { r.errorHandler = h } +// DefaultContext sets the default context for this router. +// This context will be used for all interaction events. +func (r *Mux) DefaultContext(ctx func() context.Context) { + r.defaultContext = ctx +} + func checkPattern(pattern string) { if len(pattern) == 0 { panic("pattern must not be empty") diff --git a/handler/mux_test.go b/handler/mux_test.go new file mode 100644 index 000000000..b0982c6e8 --- /dev/null +++ b/handler/mux_test.go @@ -0,0 +1,175 @@ +package handler + +import ( + "os" + "testing" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/rest" + "github.com/stretchr/testify/assert" +) + +func NewRecorder() *InteractionResponseRecorder { + return &InteractionResponseRecorder{} +} + +type InteractionResponseRecorder struct { + Response *discord.InteractionResponse +} + +func (i *InteractionResponseRecorder) Respond(responseType discord.InteractionResponseType, data discord.InteractionResponseData, opts ...rest.RequestOpt) error { + i.Response = &discord.InteractionResponse{ + Type: responseType, + Data: data, + } + return nil +} + +func TestCommandMux(t *testing.T) { + slashData, err := os.ReadFile("testdata/command/slash_command.json") + assert.NoError(t, err) + + userData, err := os.ReadFile("testdata/command/user_command.json") + assert.NoError(t, err) + + messageData, err := os.ReadFile("testdata/command/message_command.json") + assert.NoError(t, err) + + entryPointData, err := os.ReadFile("testdata/command/entry_point_command.json") + assert.NoError(t, err) + + data := []struct { + data []byte + expected *discord.InteractionResponse + }{ + { + data: slashData, + expected: &discord.InteractionResponse{ + Type: discord.InteractionResponseTypeCreateMessage, + Data: discord.MessageCreate{ + Content: "bar", + }, + }, + }, + { + data: userData, + expected: &discord.InteractionResponse{ + Type: discord.InteractionResponseTypeCreateMessage, + Data: discord.MessageCreate{ + Content: "bar2", + }, + }, + }, + { + data: messageData, + expected: &discord.InteractionResponse{ + Type: discord.InteractionResponseTypeCreateMessage, + Data: discord.MessageCreate{ + Content: "bar3", + }, + }, + }, + { + data: entryPointData, + expected: &discord.InteractionResponse{ + Type: discord.InteractionResponseTypeCreateMessage, + Data: discord.MessageCreate{ + Content: "bar4", + }, + }, + }, + } + + mux := New() + mux.SlashCommand("/foo", func(data discord.SlashCommandInteractionData, e *CommandEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "bar", + }) + }) + mux.UserCommand("/foo", func(data discord.UserCommandInteractionData, e *CommandEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "bar2", + }) + }) + mux.MessageCommand("/foo", func(data discord.MessageCommandInteractionData, e *CommandEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "bar3", + }) + }) + mux.EntryPointCommand("/foo", func(data discord.EntryPointCommandInteractionData, e *CommandEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "bar4", + }) + }) + + for _, d := range data { + interaction, err := discord.UnmarshalInteraction(d.data) + assert.NoError(t, err) + + recorder := NewRecorder() + mux.OnEvent(&events.InteractionCreate{ + GenericEvent: events.NewGenericEvent(nil, 0, 0), + Interaction: interaction, + Respond: recorder.Respond, + }) + assert.Equal(t, d.expected, recorder.Response) + } +} + +func TestComponentMux(t *testing.T) { + buttonData, err := os.ReadFile("testdata/component/button_component.json") + assert.NoError(t, err) + + selectMenuData, err := os.ReadFile("testdata/component/select_menu_component.json") + assert.NoError(t, err) + + data := []struct { + data []byte + expected *discord.InteractionResponse + }{ + { + data: buttonData, + expected: &discord.InteractionResponse{ + Type: discord.InteractionResponseTypeCreateMessage, + Data: discord.MessageCreate{ + Content: "bar", + }, + }, + }, + { + data: selectMenuData, + expected: &discord.InteractionResponse{ + Type: discord.InteractionResponseTypeCreateMessage, + Data: discord.MessageCreate{ + Content: "bar2", + }, + }, + }, + } + + mux := New() + mux.ButtonComponent("/foo", func(data discord.ButtonInteractionData, e *ComponentEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "bar", + }) + }) + mux.SelectMenuComponent("/foo", func(data discord.SelectMenuInteractionData, e *ComponentEvent) error { + return e.CreateMessage(discord.MessageCreate{ + Content: "bar2", + }) + }) + + for _, d := range data { + interaction, err := discord.UnmarshalInteraction(d.data) + assert.NoError(t, err) + + recorder := NewRecorder() + mux.OnEvent(&events.InteractionCreate{ + GenericEvent: events.NewGenericEvent(nil, 0, 0), + Interaction: interaction, + Respond: recorder.Respond, + }) + assert.Equal(t, d.expected, recorder.Response) + } +} diff --git a/handler/router.go b/handler/router.go index 33cc71319..4f36e51d7 100644 --- a/handler/router.go +++ b/handler/router.go @@ -3,16 +3,22 @@ package handler import ( "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" - "github.com/disgoorg/disgo/events" ) type ( - CommandHandler func(e *CommandEvent) error - AutocompleteHandler func(e *AutocompleteEvent) error - ComponentHandler func(e *ComponentEvent) error - ModalHandler func(e *ModalEvent) error - NotFoundHandler func(e *events.InteractionCreate) error - ErrorHandler func(e *events.InteractionCreate, err error) + InteractionHandler func(e *InteractionEvent) error + CommandHandler func(e *CommandEvent) error + SlashCommandHandler func(data discord.SlashCommandInteractionData, e *CommandEvent) error + UserCommandHandler func(data discord.UserCommandInteractionData, e *CommandEvent) error + MessageCommandHandler func(data discord.MessageCommandInteractionData, e *CommandEvent) error + EntryPointCommandHandler func(data discord.EntryPointCommandInteractionData, e *CommandEvent) error + AutocompleteHandler func(e *AutocompleteEvent) error + ComponentHandler func(e *ComponentEvent) error + ButtonComponentHandler func(data discord.ButtonInteractionData, e *ComponentEvent) error + SelectMenuComponentHandler func(data discord.SelectMenuInteractionData, e *ComponentEvent) error + ModalHandler func(e *ModalEvent) error + NotFoundHandler func(e *InteractionEvent) error + ErrorHandler func(e *InteractionEvent, err error) ) var ( @@ -26,10 +32,10 @@ var ( // Route is a basic interface for a route in a Router. type Route interface { // Match returns true if the given path matches the Route. - Match(path string, t discord.InteractionType) bool + Match(path string, t discord.InteractionType, t2 int) bool // Handle handles the given interaction event. - Handle(path string, variables map[string]string, e *events.InteractionCreate) error + Handle(path string, e *InteractionEvent) error } // Router provides with the core routing functionality. @@ -53,15 +59,36 @@ type Router interface { // Mount mounts the given router with the given pattern to the current Router. Mount(pattern string, r Router) + // Interaction registers the given InteractionHandler to the current Router. + Interaction(pattern string, h InteractionHandler) + // Command registers the given CommandHandler to the current Router. Command(pattern string, h CommandHandler) + // SlashCommand registers the given SlashCommandHandler to the current Router. + SlashCommand(pattern string, h SlashCommandHandler) + + // UserCommand registers the given UserCommandHandler to the current Router. + UserCommand(pattern string, h UserCommandHandler) + + // MessageCommand registers the given MessageCommandHandler to the current Router. + MessageCommand(pattern string, h MessageCommandHandler) + + // EntryPointCommand registers the given EntryPointCommandHandler to the current Router. + EntryPointCommand(pattern string, h EntryPointCommandHandler) + // Autocomplete registers the given AutocompleteHandler to the current Router. Autocomplete(pattern string, h AutocompleteHandler) // Component registers the given ComponentHandler to the current Router. Component(pattern string, h ComponentHandler) + // ButtonComponent registers the given ButtonComponentHandler to the current Router. + ButtonComponent(pattern string, h ButtonComponentHandler) + + // SelectMenuComponent registers the given SelectMenuComponentHandler to the current Router. + SelectMenuComponent(pattern string, h SelectMenuComponentHandler) + // Modal registers the given ModalHandler to the current Router. Modal(pattern string, h ModalHandler) } diff --git a/handler/testdata/command/entry_point_command.json b/handler/testdata/command/entry_point_command.json new file mode 100644 index 000000000..5504b3842 --- /dev/null +++ b/handler/testdata/command/entry_point_command.json @@ -0,0 +1,36 @@ +{ + "application_id": "775799577604522054", + "channel_id": "772908445358620702", + "data": { + "id": "866818195033292851", + "name": "foo", + "type": 4 + }, + "guild_id": "772904309264089089", + "guild_locale": "en-US", + "app_permissions": "442368", + "id": "867793873336926249", + "locale": "en-US", + "member": { + "avatar": null, + "deaf": false, + "is_pending": false, + "joined_at": "2020-11-02T20:46:57.364000+00:00", + "mute": false, + "nick": null, + "pending": false, + "permissions": "274877906943", + "premium_since": null, + "roles": ["785609923542777878"], + "user": { + "avatar": "a_f03401914fb4f3caa9037578ab980920", + "discriminator": "6538", + "id": "167348773423415296", + "public_flags": 1, + "username": "ian" + } + }, + "token": "UNIQUE_TOKEN", + "type": 2, + "version": 1 +} \ No newline at end of file diff --git a/handler/testdata/command/message_command.json b/handler/testdata/command/message_command.json new file mode 100644 index 000000000..b5879870d --- /dev/null +++ b/handler/testdata/command/message_command.json @@ -0,0 +1,65 @@ +{ + "application_id": "775799577604522054", + "channel_id": "772908445358620702", + "data": { + "id": "866818195033292851", + "name": "foo", + "resolved": { + "messages": { + "867793854505943041": { + "attachments": [], + "author": { + "avatar": "a_f03401914fb4f3caa9037578ab980920", + "discriminator": "6538", + "id": "167348773423415296", + "public_flags": 1, + "username": "ian" + }, + "channel_id": "772908445358620702", + "components": [], + "content": "some message", + "edited_timestamp": null, + "embeds": [], + "flags": 0, + "id": "867793854505943041", + "mention_everyone": false, + "mention_roles": [], + "mentions": [], + "pinned": false, + "timestamp": "2021-07-22T15:42:57.744000+00:00", + "tts": false, + "type": 0 + } + } + }, + "target_id": "867793854505943041", + "type": 3 + }, + "guild_id": "772904309264089089", + "guild_locale": "en-US", + "app_permissions": "442368", + "id": "867793873336926249", + "locale": "en-US", + "member": { + "avatar": null, + "deaf": false, + "is_pending": false, + "joined_at": "2020-11-02T20:46:57.364000+00:00", + "mute": false, + "nick": null, + "pending": false, + "permissions": "274877906943", + "premium_since": null, + "roles": ["785609923542777878"], + "user": { + "avatar": "a_f03401914fb4f3caa9037578ab980920", + "discriminator": "6538", + "id": "167348773423415296", + "public_flags": 1, + "username": "ian" + } + }, + "token": "UNIQUE_TOKEN", + "type": 2, + "version": 1 +} \ No newline at end of file diff --git a/handler/testdata/command/slash_command.json b/handler/testdata/command/slash_command.json new file mode 100644 index 000000000..19fd0ce46 --- /dev/null +++ b/handler/testdata/command/slash_command.json @@ -0,0 +1,33 @@ +{ + "type": 2, + "token": "A_UNIQUE_TOKEN", + "member": { + "user": { + "id": "53908232506183680", + "username": "Mason", + "avatar": "a_d5efa99b3eeaa7dd43acca82f5692432", + "discriminator": "1337", + "public_flags": 131141 + }, + "roles": ["539082325061836999"], + "premium_since": null, + "permissions": "2147483647", + "pending": false, + "nick": null, + "mute": false, + "joined_at": "2017-03-13T19:19:14.040000+00:00", + "is_pending": false, + "deaf": false + }, + "id": "786008729715212338", + "guild_id": "290926798626357999", + "app_permissions": "442368", + "guild_locale": "en-US", + "locale": "en-US", + "data": { + "type": 1, + "name": "foo", + "id": "771825006014889984" + }, + "channel_id": "645027906669510667" +} \ No newline at end of file diff --git a/handler/testdata/command/user_command.json b/handler/testdata/command/user_command.json new file mode 100644 index 000000000..817e16bda --- /dev/null +++ b/handler/testdata/command/user_command.json @@ -0,0 +1,61 @@ +{ + "application_id": "775799577604522054", + "channel_id": "772908445358620702", + "data": { + "id": "866818195033292850", + "name": "foo", + "resolved": { + "members": { + "809850198683418695": { + "avatar": null, + "is_pending": false, + "joined_at": "2021-02-12T18:25:07.972000+00:00", + "nick": null, + "pending": false, + "permissions": "246997699136", + "premium_since": null, + "roles": [] + } + }, + "users": { + "809850198683418695": { + "avatar": "afc428077119df8aabbbd84b0dc90c74", + "bot": true, + "discriminator": "7302", + "id": "809850198683418695", + "public_flags": 0, + "username": "VoltyDemo" + } + } + }, + "target_id": "809850198683418695", + "type": 2 + }, + "guild_id": "772904309264089089", + "guild_locale": "en-US", + "app_permissions": "442368", + "id": "867794291820986368", + "locale": "en-US", + "member": { + "avatar": null, + "deaf": false, + "is_pending": false, + "joined_at": "2020-11-02T20:46:57.364000+00:00", + "mute": false, + "nick": null, + "pending": false, + "permissions": "274877906943", + "premium_since": null, + "roles": ["785609923542777878"], + "user": { + "avatar": "a_f03401914fb4f3caa9037578ab980920", + "discriminator": "6538", + "id": "167348773423415296", + "public_flags": 1, + "username": "ian" + } + }, + "token": "UNIQUE_TOKEN", + "type": 2, + "version": 1 +} \ No newline at end of file diff --git a/handler/testdata/component/button_component.json b/handler/testdata/component/button_component.json new file mode 100644 index 000000000..179702eae --- /dev/null +++ b/handler/testdata/component/button_component.json @@ -0,0 +1,70 @@ +{ + "version": 1, + "type": 3, + "token": "unique_interaction_token", + "message": { + "type": 0, + "tts": false, + "timestamp": "2021-05-19T02:12:51.710000+00:00", + "pinned": false, + "mentions": [], + "mention_roles": [], + "mention_everyone": false, + "id": "844397162624450620", + "flags": 0, + "embeds": [], + "edited_timestamp": null, + "content": "This is a message with components.", + "components": [ + { + "type": 1, + "components": [ + { + "type": 2, + "label": "Click me!", + "style": 1, + "custom_id": "foo" + } + ] + } + ], + "channel_id": "345626669114982402", + "author": { + "username": "Mason", + "public_flags": 131141, + "id": "53908232506183680", + "discriminator": "1337", + "avatar": "a_d5efa99b3eeaa7dd43acca82f5692432" + }, + "attachments": [] + }, + "member": { + "user": { + "username": "Mason", + "public_flags": 131141, + "id": "53908232506183680", + "discriminator": "1337", + "avatar": "a_d5efa99b3eeaa7dd43acca82f5692432" + }, + "roles": [ + "290926798626357999" + ], + "premium_since": null, + "permissions": "17179869183", + "pending": false, + "nick": null, + "mute": false, + "joined_at": "2017-03-13T19:19:14.040000+00:00", + "is_pending": false, + "deaf": false, + "avatar": null + }, + "id": "846462639134605312", + "guild_id": "290926798626357999", + "data": { + "custom_id": "foo", + "component_type": 2 + }, + "channel_id": "345626669114982999", + "application_id": "290926444748734465" +} \ No newline at end of file diff --git a/handler/testdata/component/select_menu_component.json b/handler/testdata/component/select_menu_component.json new file mode 100644 index 000000000..ca703c268 --- /dev/null +++ b/handler/testdata/component/select_menu_component.json @@ -0,0 +1,119 @@ +{ + "application_id": "845027738276462632", + "channel_id": "772908445358620702", + "data": { + "component_type":3, + "custom_id": "foo", + "values": [ + "mage", + "rogue" + ] + }, + "guild_id": "772904309264089089", + "id": "847587388497854464", + "member": { + "avatar": null, + "deaf": false, + "is_pending": false, + "joined_at": "2020-11-02T19:25:47.248000+00:00", + "mute": false, + "nick": "Bot Man", + "pending": false, + "permissions": "17179869183", + "premium_since": null, + "roles": [ + "785609923542777878" + ], + "user":{ + "avatar": "a_d5efa99b3eeaa7dd43acca82f5692432", + "discriminator": "1337", + "id": "53908232506183680", + "public_flags": 131141, + "username": "Mason" + } + }, + "message":{ + "application_id": "845027738276462632", + "attachments": [], + "author": { + "avatar": null, + "bot": true, + "discriminator": "5284", + "id": "845027738276462632", + "public_flags": 0, + "username": "Interactions Test" + }, + "channel_id": "772908445358620702", + "components": [ + { + "components": [ + { + "custom_id": "foo", + "max_values": 1, + "min_values": 1, + "options": [ + { + "description": "Sneak n stab", + "emoji":{ + "id": "625891304148303894", + "name": "rogue" + }, + "label": "Rogue", + "value": "rogue" + }, + { + "description": "Turn 'em into a sheep", + "emoji":{ + "id": "625891304081063986", + "name": "mage" + }, + "label": "Mage", + "value": "mage" + }, + { + "description": "You get heals when I'm done doing damage", + "emoji":{ + "id": "625891303795982337", + "name": "priest" + }, + "label": "Priest", + "value": "priest" + } + ], + "placeholder": "Choose a class", + "type": 3 + } + ], + "type": 1 + } + ], + "content": "Mason is looking for new arena partners. What classes do you play?", + "edited_timestamp": null, + "embeds": [], + "flags": 0, + "id": "847587334500646933", + "interaction": { + "id": "847587333942935632", + "name": "dropdown", + "type": 2, + "user": { + "avatar": "a_d5efa99b3eeaa7dd43acca82f5692432", + "discriminator": "1337", + "id": "53908232506183680", + "public_flags": 131141, + "username": "Mason" + } + }, + "mention_everyone": false, + "mention_roles":[], + "mentions":[], + "pinned": false, + "timestamp": "2021-05-27T21:29:27.956000+00:00", + "tts": false, + "type": 20, + "webhook_id": "845027738276462632" + }, + "token": "UNIQUE_TOKEN", + "type": 3, + "version": 1 +} \ No newline at end of file diff --git a/handlers/all_handlers.go b/handlers/all_handlers.go index 5e1f2b5b0..d3872cd2c 100644 --- a/handlers/all_handlers.go +++ b/handlers/all_handlers.go @@ -48,6 +48,10 @@ var allEventHandlers = []bot.GatewayEventHandler{ bot.NewGatewayEventHandler(gateway.EventTypeChannelDelete, gatewayHandlerChannelDelete), bot.NewGatewayEventHandler(gateway.EventTypeChannelPinsUpdate, gatewayHandlerChannelPinsUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeEntitlementCreate, gatewayHandlerEntitlementCreate), + bot.NewGatewayEventHandler(gateway.EventTypeEntitlementUpdate, gatewayHandlerEntitlementUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeEntitlementDelete, gatewayHandlerEntitlementDelete), + bot.NewGatewayEventHandler(gateway.EventTypeThreadCreate, gatewayHandlerThreadCreate), bot.NewGatewayEventHandler(gateway.EventTypeThreadUpdate, gatewayHandlerThreadUpdate), bot.NewGatewayEventHandler(gateway.EventTypeThreadDelete, gatewayHandlerThreadDelete), @@ -83,6 +87,12 @@ var allEventHandlers = []bot.GatewayEventHandler{ bot.NewGatewayEventHandler(gateway.EventTypeGuildScheduledEventUserAdd, gatewayHandlerGuildScheduledEventUserAdd), bot.NewGatewayEventHandler(gateway.EventTypeGuildScheduledEventUserRemove, gatewayHandlerGuildScheduledEventUserRemove), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundCreate, gatewayHandlerGuildSoundboardSoundCreate), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundUpdate, gatewayHandlerGuildSoundboardSoundUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundDelete, gatewayHandlerGuildSoundboardSoundDelete), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundsUpdate, gatewayHandlerGuildSoundboardSoundsUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeSoundboardSounds, gatewayHandlerSoundboardSounds), + bot.NewGatewayEventHandler(gateway.EventTypeIntegrationCreate, gatewayHandlerIntegrationCreate), bot.NewGatewayEventHandler(gateway.EventTypeIntegrationUpdate, gatewayHandlerIntegrationUpdate), bot.NewGatewayEventHandler(gateway.EventTypeIntegrationDelete, gatewayHandlerIntegrationDelete), @@ -97,6 +107,9 @@ var allEventHandlers = []bot.GatewayEventHandler{ bot.NewGatewayEventHandler(gateway.EventTypeMessageDelete, gatewayHandlerMessageDelete), bot.NewGatewayEventHandler(gateway.EventTypeMessageDeleteBulk, gatewayHandlerMessageDeleteBulk), + bot.NewGatewayEventHandler(gateway.EventTypeMessagePollVoteAdd, gatewayHandlerMessagePollVoteAdd), + bot.NewGatewayEventHandler(gateway.EventTypeMessagePollVoteRemove, gatewayHandlerMessagePollVoteRemove), + bot.NewGatewayEventHandler(gateway.EventTypeMessageReactionAdd, gatewayHandlerMessageReactionAdd), bot.NewGatewayEventHandler(gateway.EventTypeMessageReactionRemove, gatewayHandlerMessageReactionRemove), bot.NewGatewayEventHandler(gateway.EventTypeMessageReactionRemoveAll, gatewayHandlerMessageReactionRemoveAll), @@ -108,9 +121,14 @@ var allEventHandlers = []bot.GatewayEventHandler{ bot.NewGatewayEventHandler(gateway.EventTypeStageInstanceUpdate, gatewayHandlerStageInstanceUpdate), bot.NewGatewayEventHandler(gateway.EventTypeStageInstanceDelete, gatewayHandlerStageInstanceDelete), + bot.NewGatewayEventHandler(gateway.EventTypeSubscriptionCreate, gatewayHandlerSubscriptionCreate), + bot.NewGatewayEventHandler(gateway.EventTypeSubscriptionUpdate, gatewayHandlerSubscriptionUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeSubscriptionDelete, gatewayHandlerSubscriptionDelete), + bot.NewGatewayEventHandler(gateway.EventTypeTypingStart, gatewayHandlerTypingStart), bot.NewGatewayEventHandler(gateway.EventTypeUserUpdate, gatewayHandlerUserUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeVoiceChannelEffectSend, gatewayHandlerVoiceChannelEffectSend), bot.NewGatewayEventHandler(gateway.EventTypeVoiceStateUpdate, gatewayHandlerVoiceStateUpdate), bot.NewGatewayEventHandler(gateway.EventTypeVoiceServerUpdate, gatewayHandlerVoiceServerUpdate), diff --git a/handlers/entitlement_handlers.go b/handlers/entitlement_handlers.go new file mode 100644 index 000000000..68662a093 --- /dev/null +++ b/handlers/entitlement_handlers.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" +) + +func gatewayHandlerEntitlementCreate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventEntitlementCreate) { + client.EventManager().DispatchEvent(&events.EntitlementCreate{ + GenericEntitlementEvent: &events.GenericEntitlementEvent{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + Entitlement: event.Entitlement, + }, + }) +} + +func gatewayHandlerEntitlementUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventEntitlementUpdate) { + client.EventManager().DispatchEvent(&events.EntitlementUpdate{ + GenericEntitlementEvent: &events.GenericEntitlementEvent{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + Entitlement: event.Entitlement, + }, + }) +} + +func gatewayHandlerEntitlementDelete(client bot.Client, sequenceNumber int, shardID int, event gateway.EventEntitlementDelete) { + client.EventManager().DispatchEvent(&events.EntitlementDelete{ + GenericEntitlementEvent: &events.GenericEntitlementEvent{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + Entitlement: event.Entitlement, + }, + }) +} diff --git a/handlers/guild_emojis_update_handler.go b/handlers/guild_emojis_update_handler.go index a3477097a..b1b608335 100644 --- a/handlers/guild_emojis_update_handler.go +++ b/handlers/guild_emojis_update_handler.go @@ -1,14 +1,14 @@ package handlers import ( - "github.com/disgoorg/snowflake/v2" - "golang.org/x/exp/slices" + "slices" "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/snowflake/v2" ) type updatedEmoji struct { diff --git a/handlers/guild_handlers.go b/handlers/guild_handlers.go index 1b3f03ad9..c58bf0829 100644 --- a/handlers/guild_handlers.go +++ b/handlers/guild_handlers.go @@ -1,6 +1,8 @@ package handlers import ( + "log/slog" + "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" @@ -56,6 +58,10 @@ func gatewayHandlerGuildCreate(client bot.Client, sequenceNumber int, shardID in client.Caches().AddGuildScheduledEvent(guildScheduledEvent) } + for _, soundboardSound := range event.SoundboardSounds { + client.Caches().AddGuildSoundboardSound(soundboardSound) + } + for _, presence := range event.Presences { presence.GuildID = event.ID // populate unset field client.Caches().AddPresence(presence) @@ -80,11 +86,12 @@ func gatewayHandlerGuildCreate(client bot.Client, sequenceNumber int, shardID in if client.MemberChunkingManager().MemberChunkingFilter()(event.ID) { go func() { if _, err := client.MemberChunkingManager().RequestMembersWithQuery(event.ID, "", 0); err != nil { - client.Logger().Error("failed to chunk guild on guild_create. error: ", err) + client.Logger().Error("failed to chunk guild on guild_create", slog.Any("err", err)) } }() } + return } if wasUnavailable { client.Caches().SetGuildUnavailable(event.ID, false) @@ -112,6 +119,10 @@ func gatewayHandlerGuildUpdate(client bot.Client, sequenceNumber int, shardID in } func gatewayHandlerGuildDelete(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildDelete) { + if event.Unavailable { + client.Caches().SetGuildUnavailable(event.ID, true) + } + guild, _ := client.Caches().RemoveGuild(event.ID) client.Caches().RemoveVoiceStatesByGuildID(event.ID) client.Caches().RemovePresencesByGuildID(event.ID) @@ -125,13 +136,12 @@ func gatewayHandlerGuildDelete(client bot.Client, sequenceNumber int, shardID in client.Caches().RemoveEmojisByGuildID(event.ID) client.Caches().RemoveStickersByGuildID(event.ID) client.Caches().RemoveRolesByGuildID(event.ID) + client.Caches().RemoveMembersByGuildID(event.ID) client.Caches().RemoveStageInstancesByGuildID(event.ID) + client.Caches().RemoveGuildScheduledEventsByGuildID(event.ID) + client.Caches().RemoveGuildSoundboardSoundsByGuildID(event.ID) client.Caches().RemoveMessagesByGuildID(event.ID) - if event.Unavailable { - client.Caches().SetGuildUnavailable(event.ID, true) - } - genericGuildEvent := &events.GenericGuild{ GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), GuildID: event.ID, diff --git a/handlers/guild_soundboard_handlers.go b/handlers/guild_soundboard_handlers.go new file mode 100644 index 000000000..6c775bfd3 --- /dev/null +++ b/handlers/guild_soundboard_handlers.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" +) + +func gatewayHandlerGuildSoundboardSoundCreate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundCreate) { + client.Caches().AddGuildSoundboardSound(event.SoundboardSound) + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundCreate{ + GenericGuildSoundboardSound: &events.GenericGuildSoundboardSound{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSound: event.SoundboardSound, + }, + }) +} + +func gatewayHandlerGuildSoundboardSoundUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundUpdate) { + oldSound, _ := client.Caches().GuildSoundboardSound(*event.GuildID, event.SoundID) + client.Caches().AddGuildSoundboardSound(event.SoundboardSound) + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundUpdate{ + GenericGuildSoundboardSound: &events.GenericGuildSoundboardSound{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSound: event.SoundboardSound, + }, + OldGuildSoundboardSound: oldSound, + }) +} + +func gatewayHandlerGuildSoundboardSoundDelete(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundDelete) { + client.Caches().RemoveGuildSoundboardSound(event.GuildID, event.SoundID) + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundDelete{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundID: event.SoundID, + GuildID: event.GuildID, + }) +} + +func gatewayHandlerGuildSoundboardSoundsUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundsUpdate) { + for _, sound := range event.SoundboardSounds { + client.Caches().AddGuildSoundboardSound(sound) + } + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundsUpdate{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSounds: event.SoundboardSounds, + GuildID: event.GuildID, + }) +} + +func gatewayHandlerSoundboardSounds(client bot.Client, sequenceNumber int, shardID int, event gateway.EventSoundboardSounds) { + client.EventManager().DispatchEvent(&events.SoundboardSounds{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSounds: event.SoundboardSounds, + GuildID: event.GuildID, + }) +} diff --git a/handlers/interaction_create_handler.go b/handlers/interaction_create_handler.go index 9c7988eb9..c93ccbc0a 100644 --- a/handlers/interaction_create_handler.go +++ b/handlers/interaction_create_handler.go @@ -1,6 +1,9 @@ package handlers import ( + "fmt" + "log/slog" + "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" @@ -65,6 +68,6 @@ func handleInteraction(client bot.Client, sequenceNumber int, shardID int, respo }) default: - client.Logger().Errorf("unknown interaction with type %T received", interaction) + client.Logger().Error("unknown interaction", slog.String("type", fmt.Sprintf("%T", interaction))) } } diff --git a/handlers/interaction_create_http_handler.go b/handlers/interaction_create_http_handler.go index 0d2e43626..3124da979 100644 --- a/handlers/interaction_create_http_handler.go +++ b/handlers/interaction_create_http_handler.go @@ -1,6 +1,8 @@ package handlers import ( + "log/slog" + "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/httpserver" @@ -18,7 +20,7 @@ func (h *httpserverHandlerInteractionCreate) HandleHTTPEvent(client bot.Client, if err := respondFunc(discord.InteractionResponse{ Type: discord.InteractionResponseTypePong, }); err != nil { - client.Logger().Error("failed to respond to http interaction ping: ", err) + client.Logger().Error("failed to respond to http interaction ping", slog.Any("err", err)) } return } diff --git a/handlers/invite_handlers.go b/handlers/invite_handlers.go index 9ea078e88..d419be381 100644 --- a/handlers/invite_handlers.go +++ b/handlers/invite_handlers.go @@ -1,37 +1,23 @@ package handlers import ( - "github.com/disgoorg/snowflake/v2" - "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" ) func gatewayHandlerInviteCreate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventInviteCreate) { - var guildID *snowflake.ID - if event.Guild != nil { - guildID = &event.Guild.ID - } - client.EventManager().DispatchEvent(&events.InviteCreate{ - GenericInvite: &events.GenericInvite{ - GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), - GuildID: guildID, - Code: event.Code, - ChannelID: event.ChannelID, - }, - Invite: event.Invite, + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + EventInviteCreate: event, }) } func gatewayHandlerInviteDelete(client bot.Client, sequenceNumber int, shardID int, event gateway.EventInviteDelete) { client.EventManager().DispatchEvent(&events.InviteDelete{ - GenericInvite: &events.GenericInvite{ - GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), - GuildID: event.GuildID, - ChannelID: event.ChannelID, - Code: event.Code, - }, + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + GuildID: event.GuildID, + ChannelID: event.ChannelID, + Code: event.Code, }) } diff --git a/handlers/message_poll_handler.go b/handlers/message_poll_handler.go new file mode 100644 index 000000000..affdecc0a --- /dev/null +++ b/handlers/message_poll_handler.go @@ -0,0 +1,83 @@ +package handlers + +import ( + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" +) + +func gatewayHandlerMessagePollVoteAdd(client bot.Client, sequenceNumber int, shardID int, event gateway.EventMessagePollVoteAdd) { + genericEvent := events.NewGenericEvent(client, sequenceNumber, shardID) + + client.EventManager().DispatchEvent(&events.MessagePollVoteAdd{ + GenericMessagePollVote: &events.GenericMessagePollVote{ + GenericEvent: genericEvent, + UserID: event.UserID, + ChannelID: event.ChannelID, + MessageID: event.MessageID, + GuildID: event.GuildID, + AnswerID: event.AnswerID, + }, + }) + + if event.GuildID == nil { + client.EventManager().DispatchEvent(&events.DMMessagePollVoteAdd{ + GenericDMMessagePollVote: &events.GenericDMMessagePollVote{ + GenericEvent: genericEvent, + UserID: event.UserID, + ChannelID: event.ChannelID, + MessageID: event.MessageID, + AnswerID: event.AnswerID, + }, + }) + } else { + client.EventManager().DispatchEvent(&events.GuildMessagePollVoteAdd{ + GenericGuildMessagePollVote: &events.GenericGuildMessagePollVote{ + GenericEvent: genericEvent, + UserID: event.UserID, + ChannelID: event.ChannelID, + MessageID: event.MessageID, + GuildID: *event.GuildID, + AnswerID: event.AnswerID, + }, + }) + } +} + +func gatewayHandlerMessagePollVoteRemove(client bot.Client, sequenceNumber int, shardID int, event gateway.EventMessagePollVoteRemove) { + genericEvent := events.NewGenericEvent(client, sequenceNumber, shardID) + + client.EventManager().DispatchEvent(&events.MessagePollVoteRemove{ + GenericMessagePollVote: &events.GenericMessagePollVote{ + GenericEvent: genericEvent, + UserID: event.UserID, + ChannelID: event.ChannelID, + MessageID: event.MessageID, + GuildID: event.GuildID, + AnswerID: event.AnswerID, + }, + }) + + if event.GuildID == nil { + client.EventManager().DispatchEvent(&events.DMMessagePollVoteRemove{ + GenericDMMessagePollVote: &events.GenericDMMessagePollVote{ + GenericEvent: genericEvent, + UserID: event.UserID, + ChannelID: event.ChannelID, + MessageID: event.MessageID, + AnswerID: event.AnswerID, + }, + }) + } else { + client.EventManager().DispatchEvent(&events.GuildMessagePollVoteRemove{ + GenericGuildMessagePollVote: &events.GenericGuildMessagePollVote{ + GenericEvent: genericEvent, + UserID: event.UserID, + ChannelID: event.ChannelID, + MessageID: event.MessageID, + GuildID: *event.GuildID, + AnswerID: event.AnswerID, + }, + }) + } +} diff --git a/handlers/message_reaction_handler.go b/handlers/message_reaction_handler.go index 1b1eb6884..0e7386816 100644 --- a/handlers/message_reaction_handler.go +++ b/handlers/message_reaction_handler.go @@ -2,6 +2,7 @@ package handlers import ( "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" ) @@ -37,6 +38,11 @@ func gatewayHandlerMessageReactionAdd(client bot.Client, sequenceNumber int, sha MessageAuthorID: event.MessageAuthorID, }) } else { + var member discord.Member + // sometimes the member is nil for some reason + if event.Member != nil { + member = *event.Member + } client.EventManager().DispatchEvent(&events.GuildMessageReactionAdd{ GenericGuildMessageReaction: &events.GenericGuildMessageReaction{ GenericEvent: genericEvent, @@ -48,7 +54,7 @@ func gatewayHandlerMessageReactionAdd(client bot.Client, sequenceNumber int, sha BurstColors: event.BurstColors, Burst: event.Burst, }, - Member: *event.Member, + Member: member, MessageAuthorID: event.MessageAuthorID, }) } diff --git a/handlers/presence_update_handler.go b/handlers/presence_update_handler.go index 2267796f5..997082596 100644 --- a/handlers/presence_update_handler.go +++ b/handlers/presence_update_handler.go @@ -1,13 +1,14 @@ package handlers import ( + "slices" + "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/cache" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/gateway" "github.com/disgoorg/snowflake/v2" - "golang.org/x/exp/slices" ) func gatewayHandlerPresenceUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventPresenceUpdate) { diff --git a/handlers/subscription_handlers.go b/handlers/subscription_handlers.go new file mode 100644 index 000000000..05ad8e219 --- /dev/null +++ b/handlers/subscription_handlers.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" +) + +func gatewayHandlerSubscriptionCreate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventSubscriptionCreate) { + client.EventManager().DispatchEvent(&events.SubscriptionCreate{ + GenericSubscriptionEvent: &events.GenericSubscriptionEvent{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + Subscription: event.Subscription, + }, + }) +} + +func gatewayHandlerSubscriptionUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventSubscriptionUpdate) { + client.EventManager().DispatchEvent(&events.SubscriptionUpdate{ + GenericSubscriptionEvent: &events.GenericSubscriptionEvent{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + Subscription: event.Subscription, + }, + }) +} + +func gatewayHandlerSubscriptionDelete(client bot.Client, sequenceNumber int, shardID int, event gateway.EventSubscriptionDelete) { + client.EventManager().DispatchEvent(&events.SubscriptionDelete{ + GenericSubscriptionEvent: &events.GenericSubscriptionEvent{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + Subscription: event.Subscription, + }, + }) +} diff --git a/handlers/thread_handler.go b/handlers/thread_handler.go index 3058ddf3e..f77b23353 100644 --- a/handlers/thread_handler.go +++ b/handlers/thread_handler.go @@ -19,6 +19,7 @@ func gatewayHandlerThreadCreate(client bot.Client, sequenceNumber int, shardID i Thread: event.GuildThread, }, ThreadMember: event.ThreadMember, + NewlyCreated: event.NewlyCreated, }) } diff --git a/handlers/voice_handlers.go b/handlers/voice_handlers.go index ea906ccc3..ca195858b 100644 --- a/handlers/voice_handlers.go +++ b/handlers/voice_handlers.go @@ -6,6 +6,13 @@ import ( "github.com/disgoorg/disgo/gateway" ) +func gatewayHandlerVoiceChannelEffectSend(client bot.Client, sequenceNumber int, shardID int, event gateway.EventVoiceChannelEffectSend) { + client.EventManager().DispatchEvent(&events.GuildVoiceChannelEffectSend{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + EventVoiceChannelEffectSend: event, + }) +} + func gatewayHandlerVoiceStateUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventVoiceStateUpdate) { member := event.Member @@ -47,7 +54,7 @@ func gatewayHandlerVoiceStateUpdate(client bot.Client, sequenceNumber int, shard OldVoiceState: oldVoiceState, }) } else { - client.Logger().Warnf("could not decide which GuildVoice to fire") + client.Logger().Warn("could not decide which GuildVoice to fire") } } diff --git a/httpserver/config.go b/httpserver/config.go index 8666813ce..92b0f231f 100644 --- a/httpserver/config.go +++ b/httpserver/config.go @@ -1,24 +1,24 @@ package httpserver import ( + "log/slog" "net/http" - - "github.com/disgoorg/log" ) // DefaultConfig returns a Config with sensible defaults. func DefaultConfig() *Config { return &Config{ - URL: "/interactions/callback", - Address: ":80", + Logger: slog.Default(), HTTPServer: &http.Server{}, ServeMux: http.NewServeMux(), + URL: "/interactions/callback", + Address: ":80", } } // Config lets you configure your Server instance. type Config struct { - Logger log.Logger + Logger *slog.Logger HTTPServer *http.Server ServeMux *http.ServeMux URL string @@ -38,7 +38,7 @@ func (c *Config) Apply(opts []ConfigOpt) { } // WithLogger sets the Logger of the Config. -func WithLogger(logger log.Logger) ConfigOpt { +func WithLogger(logger *slog.Logger) ConfigOpt { return func(config *Config) { config.Logger = logger } diff --git a/httpserver/server.go b/httpserver/server.go index ab25f3b72..af38d3cc0 100644 --- a/httpserver/server.go +++ b/httpserver/server.go @@ -5,12 +5,12 @@ import ( "context" "encoding/hex" "io" + "log/slog" "net/http" "sync" "time" "github.com/disgoorg/json" - "github.com/disgoorg/log" "github.com/disgoorg/disgo/discord" ) @@ -102,12 +102,12 @@ const ( ) // HandleInteraction handles an interaction from Discord's Outgoing Webhooks. It verifies and parses the interaction and then calls the passed EventHandlerFunc. -func HandleInteraction(publicKey PublicKey, logger log.Logger, handleFunc EventHandlerFunc) http.HandlerFunc { +func HandleInteraction(publicKey PublicKey, logger *slog.Logger, handleFunc EventHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if ok := VerifyRequest(r, publicKey); !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) data, _ := io.ReadAll(r.Body) - logger.Trace("received http interaction with invalid signature. body: ", string(data)) + logger.Debug("received http interaction with invalid signature", slog.String("body", string(data))) return } @@ -117,11 +117,11 @@ func HandleInteraction(publicKey PublicKey, logger log.Logger, handleFunc EventH buff := new(bytes.Buffer) rqData, _ := io.ReadAll(io.TeeReader(r.Body, buff)) - logger.Trace("received http interaction. body: ", string(rqData)) + logger.Debug("received http interaction", slog.String("body", string(rqData))) var v EventInteractionCreate if err := json.NewDecoder(buff).Decode(&v); err != nil { - logger.Error("error while decoding interaction: ", err) + logger.Error("error while decoding interaction", slog.Any("err", err)) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } @@ -167,6 +167,14 @@ func HandleInteraction(publicKey PublicKey, logger log.Logger, handleFunc EventH defer cancel() select { case response := <-responseChannel: + + // if we only acknowledge the interaction, we don't need to send a response body + // we just need to send a 202 Accepted status + if response.Type == discord.InteractionResponseTypeAcknowledge { + w.WriteHeader(http.StatusAccepted) + return + } + if body, err = response.ToBody(); err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) errorChannel <- err @@ -200,6 +208,6 @@ func HandleInteraction(publicKey PublicKey, logger log.Logger, handleFunc EventH } rsData, _ := io.ReadAll(rsBody) - logger.Trace("response to http interaction. body: ", string(rsData)) + logger.Debug("response to http interaction", slog.String("body", string(rsData))) } } diff --git a/httpserver/server_impl.go b/httpserver/server_impl.go index 02f12fe51..be30f6686 100644 --- a/httpserver/server_impl.go +++ b/httpserver/server_impl.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "log/slog" "net/http" ) @@ -13,10 +14,11 @@ var _ Server = (*serverImpl)(nil) func New(publicKey string, eventHandlerFunc EventHandlerFunc, opts ...ConfigOpt) Server { config := DefaultConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "httpserver")) hexDecodedKey, err := hex.DecodeString(publicKey) if err != nil { - config.Logger.Errorf("error while decoding hex string: %s", err) + config.Logger.Debug("error while decoding hex string", slog.Any("err", err)) } return &serverImpl{ @@ -45,7 +47,7 @@ func (s *serverImpl) Start() { err = s.config.HTTPServer.ListenAndServe() } if !errors.Is(err, http.ErrServerClosed) { - s.config.Logger.Error("error while running http server: ", err) + s.config.Logger.Error("error while running http server", slog.Any("err", err)) } }() } diff --git a/internal/flags/flags.go b/internal/flags/flags.go index b7899cb9d..450f6af15 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -1,9 +1,12 @@ package flags -import "golang.org/x/exp/constraints" +// Integer is a constraint that permits any integer type. +type Integer interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} // Add allows you to add multiple bits together, producing a new bit -func Add[T constraints.Integer](f T, bits ...T) T { +func Add[T Integer](f T, bits ...T) T { for _, bit := range bits { f |= bit } @@ -11,7 +14,7 @@ func Add[T constraints.Integer](f T, bits ...T) T { } // Remove allows you to subtract multiple bits from the first, producing a new bit -func Remove[T constraints.Integer](f T, bits ...T) T { +func Remove[T Integer](f T, bits ...T) T { for _, bit := range bits { f &^= bit } @@ -19,7 +22,7 @@ func Remove[T constraints.Integer](f T, bits ...T) T { } // Has will ensure that the bit includes all the bits entered -func Has[T constraints.Integer](f T, bits ...T) bool { +func Has[T Integer](f T, bits ...T) bool { for _, bit := range bits { if (f & bit) != bit { return false @@ -29,7 +32,7 @@ func Has[T constraints.Integer](f T, bits ...T) bool { } // Missing will check whether the bit is missing any one of the bits -func Missing[T constraints.Integer](f T, bits ...T) bool { +func Missing[T Integer](f T, bits ...T) bool { for _, bit := range bits { if (f & bit) != bit { return true diff --git a/internal/insecurerandstr/random_str.go b/internal/insecurerandstr/random_str.go index 360a9ec17..3f5698a27 100644 --- a/internal/insecurerandstr/random_str.go +++ b/internal/insecurerandstr/random_str.go @@ -6,17 +6,17 @@ import ( "time" ) -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +var ( + letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") -func init() { - rand.Seed(time.Now().UnixNano()) -} + randStr = rand.New(rand.NewSource(time.Now().UnixNano())) +) // RandStr returns a random string of the given length. func RandStr(n int) string { b := make([]rune, n) for i := range b { - b[i] = letters[rand.Intn(len(letters))] + b[i] = letters[randStr.Intn(len(letters))] } return string(b) } diff --git a/internal/slicehelper/slice_helper.go b/internal/slicehelper/slice_helper.go new file mode 100644 index 000000000..b2e755736 --- /dev/null +++ b/internal/slicehelper/slice_helper.go @@ -0,0 +1,14 @@ +package slicehelper + +import "github.com/disgoorg/snowflake/v2" + +func JoinSnowflakes(snowflakes []snowflake.ID) string { + var str string + for i, s := range snowflakes { + str += s.String() + if i != len(str)-1 { + str += "," + } + } + return str +} diff --git a/oauth2/client.go b/oauth2/client.go index 91cbb27d7..60a1ff668 100644 --- a/oauth2/client.go +++ b/oauth2/client.go @@ -46,6 +46,15 @@ func (s Session) Expired() bool { return s.Expiration.Before(time.Now()) } +type AuthorizationURLParams struct { + RedirectURI string + Permissions discord.Permissions + GuildID snowflake.ID + DisableGuildSelect bool + IntegrationType discord.ApplicationIntegrationType + Scopes []discord.OAuth2Scope +} + // Client is a high level wrapper around Discord's OAuth2 API. type Client interface { // ID returns the configured client ID. @@ -58,10 +67,10 @@ type Client interface { // StateController returns the configured StateController. StateController() StateController - // GenerateAuthorizationURL generates an authorization URL with the given redirect URI, permissions, guildID, disableGuildSelect & scopes. State is automatically generated. - GenerateAuthorizationURL(redirectURI string, permissions discord.Permissions, guildID snowflake.ID, disableGuildSelect bool, scopes ...discord.OAuth2Scope) string - // GenerateAuthorizationURLState generates an authorization URL with the given redirect URI, permissions, guildID, disableGuildSelect & scopes. State is automatically generated & returned. - GenerateAuthorizationURLState(redirectURI string, permissions discord.Permissions, guildID snowflake.ID, disableGuildSelect bool, scopes ...discord.OAuth2Scope) (string, string) + // GenerateAuthorizationURL generates an authorization URL with the given authorization params. State is automatically generated. + GenerateAuthorizationURL(params AuthorizationURLParams) string + // GenerateAuthorizationURLState generates an authorization URL with the given authorization params. State is automatically generated & returned. + GenerateAuthorizationURLState(params AuthorizationURLParams) (string, string) // StartSession starts a new Session with the given authorization code & state. StartSession(code string, state string, opts ...rest.RequestOpt) (Session, *discord.IncomingWebhook, error) diff --git a/oauth2/client_impl.go b/oauth2/client_impl.go index 080968bfc..9e050607a 100644 --- a/oauth2/client_impl.go +++ b/oauth2/client_impl.go @@ -1,6 +1,7 @@ package oauth2 import ( + "log/slog" "time" "github.com/disgoorg/snowflake/v2" @@ -13,6 +14,7 @@ import ( func New(id snowflake.ID, secret string, opts ...ConfigOpt) Client { config := DefaultConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "oauth2")) return &clientImpl{ id: id, @@ -45,30 +47,33 @@ func (c *clientImpl) StateController() StateController { return c.stateController } -func (c *clientImpl) GenerateAuthorizationURL(redirectURI string, permissions discord.Permissions, guildID snowflake.ID, disableGuildSelect bool, scopes ...discord.OAuth2Scope) string { - authURL, _ := c.GenerateAuthorizationURLState(redirectURI, permissions, guildID, disableGuildSelect, scopes...) +func (c *clientImpl) GenerateAuthorizationURL(params AuthorizationURLParams) string { + authURL, _ := c.GenerateAuthorizationURLState(params) return authURL } -func (c *clientImpl) GenerateAuthorizationURLState(redirectURI string, permissions discord.Permissions, guildID snowflake.ID, disableGuildSelect bool, scopes ...discord.OAuth2Scope) (string, string) { - state := c.StateController().NewState(redirectURI) +func (c *clientImpl) GenerateAuthorizationURLState(params AuthorizationURLParams) (string, string) { + state := c.StateController().NewState(params.RedirectURI) values := discord.QueryValues{ "client_id": c.id, - "redirect_uri": redirectURI, + "redirect_uri": params.RedirectURI, "response_type": "code", - "scope": discord.JoinScopes(scopes), + "scope": discord.JoinScopes(params.Scopes), "state": state, } - if permissions != discord.PermissionsNone { - values["permissions"] = permissions + if params.Permissions != discord.PermissionsNone { + values["permissions"] = params.Permissions } - if guildID != 0 { - values["guild_id"] = guildID + if params.GuildID != 0 { + values["guild_id"] = params.GuildID } - if disableGuildSelect { + if params.DisableGuildSelect { values["disable_guild_select"] = true } + if params.IntegrationType != 0 { + values["integration_type"] = params.IntegrationType + } return discord.AuthorizeURL(values), state } @@ -158,6 +163,6 @@ func newSession(accessToken discord.AccessTokenResponse) Session { RefreshToken: accessToken.RefreshToken, Scopes: accessToken.Scope, TokenType: accessToken.TokenType, - Expiration: time.Now().Add(accessToken.ExpiresIn * time.Second), + Expiration: time.Now().Add(accessToken.ExpiresIn), } } diff --git a/oauth2/config.go b/oauth2/config.go index 64c5a4074..0b17ea2b0 100644 --- a/oauth2/config.go +++ b/oauth2/config.go @@ -1,7 +1,7 @@ package oauth2 import ( - "github.com/disgoorg/log" + "log/slog" "github.com/disgoorg/disgo/rest" ) @@ -9,13 +9,13 @@ import ( // DefaultConfig is the configuration which is used by default func DefaultConfig() *Config { return &Config{ - Logger: log.Default(), + Logger: slog.Default(), } } // Config is the configuration for the OAuth2 client type Config struct { - Logger log.Logger + Logger *slog.Logger RestClient rest.Client RestClientConfigOpts []rest.ConfigOpt OAuth2 rest.OAuth2 @@ -43,7 +43,7 @@ func (c *Config) Apply(opts []ConfigOpt) { } // WithLogger applies a custom logger to the OAuth2 client -func WithLogger(logger log.Logger) ConfigOpt { +func WithLogger(logger *slog.Logger) ConfigOpt { return func(config *Config) { config.Logger = logger } diff --git a/oauth2/state_controller.go b/oauth2/state_controller.go index f239dca9f..0f5c50a72 100644 --- a/oauth2/state_controller.go +++ b/oauth2/state_controller.go @@ -1,6 +1,6 @@ package oauth2 -import "github.com/disgoorg/log" +import "log/slog" var ( _ StateController = (*stateControllerImpl)(nil) @@ -19,6 +19,7 @@ type StateController interface { func NewStateController(opts ...StateControllerConfigOpt) StateController { config := DefaultStateControllerConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "oauth2_state_controller")) states := newTTLMap(config.MaxTTL) for state, url := range config.States { @@ -33,14 +34,14 @@ func NewStateController(opts ...StateControllerConfigOpt) StateController { } type stateControllerImpl struct { - logger log.Logger + logger *slog.Logger states *ttlMap newStateFunc func() string } func (c *stateControllerImpl) NewState(redirectURI string) string { state := c.newStateFunc() - c.logger.Debugf("new state: %s for redirect uri: %s", state, redirectURI) + c.logger.Debug("new state: %s for redirect uri", slog.String("state", state), slog.String("redirect_uri", redirectURI)) c.states.put(state, redirectURI) return state } @@ -50,7 +51,7 @@ func (c *stateControllerImpl) UseState(state string) string { if uri == "" { return "" } - c.logger.Debugf("using state: %s for redirect uri: %s", state, uri) + c.logger.Debug("using state: %s for redirect uri", slog.String("state", state), slog.String("redirect_uri", uri)) c.states.delete(state) return uri } diff --git a/oauth2/state_controller_config.go b/oauth2/state_controller_config.go index e9cd8067b..02f43c69f 100644 --- a/oauth2/state_controller_config.go +++ b/oauth2/state_controller_config.go @@ -1,17 +1,16 @@ package oauth2 import ( + "log/slog" "time" - "github.com/disgoorg/log" - "github.com/disgoorg/disgo/internal/insecurerandstr" ) // DefaultStateControllerConfig is the default configuration for the StateController func DefaultStateControllerConfig() *StateControllerConfig { return &StateControllerConfig{ - Logger: log.Default(), + Logger: slog.Default(), States: map[string]string{}, NewStateFunc: func() string { return insecurerandstr.RandStr(32) }, MaxTTL: time.Hour, @@ -20,7 +19,7 @@ func DefaultStateControllerConfig() *StateControllerConfig { // StateControllerConfig is the configuration for the StateController type StateControllerConfig struct { - Logger log.Logger + Logger *slog.Logger States map[string]string NewStateFunc func() string MaxTTL time.Duration @@ -37,7 +36,7 @@ func (c *StateControllerConfig) Apply(opts []StateControllerConfigOpt) { } // WithStateControllerLogger sets the logger for the StateController -func WithStateControllerLogger(logger log.Logger) StateControllerConfigOpt { +func WithStateControllerLogger(logger *slog.Logger) StateControllerConfigOpt { return func(config *StateControllerConfig) { config.Logger = logger } diff --git a/rest/applications.go b/rest/applications.go index d68acd361..773e3b600 100644 --- a/rest/applications.go +++ b/rest/applications.go @@ -1,6 +1,7 @@ package rest import ( + "github.com/disgoorg/disgo/internal/slicehelper" "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" @@ -13,6 +14,9 @@ func NewApplications(client Client) Applications { } type Applications interface { + GetCurrentApplication(opts ...RequestOpt) (*discord.Application, error) + UpdateCurrentApplication(applicationUpdate discord.ApplicationUpdate, opts ...RequestOpt) (*discord.Application, error) + GetGlobalCommands(applicationID snowflake.ID, withLocalizations bool, opts ...RequestOpt) ([]discord.ApplicationCommand, error) GetGlobalCommand(applicationID snowflake.ID, commandID snowflake.ID, opts ...RequestOpt) (discord.ApplicationCommand, error) CreateGlobalCommand(applicationID snowflake.ID, commandCreate discord.ApplicationCommandCreate, opts ...RequestOpt) (discord.ApplicationCommand, error) @@ -32,12 +36,72 @@ type Applications interface { GetApplicationRoleConnectionMetadata(applicationID snowflake.ID, opts ...RequestOpt) ([]discord.ApplicationRoleConnectionMetadata, error) UpdateApplicationRoleConnectionMetadata(applicationID snowflake.ID, newRecords []discord.ApplicationRoleConnectionMetadata, opts ...RequestOpt) ([]discord.ApplicationRoleConnectionMetadata, error) + + GetEntitlements(applicationID snowflake.ID, params GetEntitlementsParams, opts ...RequestOpt) ([]discord.Entitlement, error) + GetEntitlement(applicationID snowflake.ID, entitlementID snowflake.ID, opts ...RequestOpt) (*discord.Entitlement, error) + CreateTestEntitlement(applicationID snowflake.ID, entitlementCreate discord.TestEntitlementCreate, opts ...RequestOpt) (*discord.Entitlement, error) + DeleteTestEntitlement(applicationID snowflake.ID, entitlementID snowflake.ID, opts ...RequestOpt) error + ConsumeEntitlement(applicationID snowflake.ID, entitlementID snowflake.ID, opts ...RequestOpt) error + + GetApplicationEmojis(applicationID snowflake.ID, opts ...RequestOpt) ([]discord.Emoji, error) + GetApplicationEmoji(applicationID snowflake.ID, emojiID snowflake.ID, opts ...RequestOpt) (*discord.Emoji, error) + CreateApplicationEmoji(applicationID snowflake.ID, emojiCreate discord.EmojiCreate, opts ...RequestOpt) (*discord.Emoji, error) + UpdateApplicationEmoji(applicationID snowflake.ID, emojiID snowflake.ID, emojiUpdate discord.EmojiUpdate, opts ...RequestOpt) (*discord.Emoji, error) + DeleteApplicationEmoji(applicationID snowflake.ID, emojiID snowflake.ID, opts ...RequestOpt) error + + GetActivityInstance(applicationID snowflake.ID, instanceID string, opts ...RequestOpt) (*discord.ActivityInstance, error) +} + +// GetEntitlementsParams holds query parameters for Applications.GetEntitlements (https://discord.com/developers/docs/resources/entitlement#list-entitlements) +type GetEntitlementsParams struct { + UserID snowflake.ID + SkuIDs []snowflake.ID + Before int + After int + Limit int + GuildID snowflake.ID + ExcludeEnded bool + ExcludeDeleted bool +} + +func (p GetEntitlementsParams) ToQueryValues() discord.QueryValues { + queryValues := discord.QueryValues{ + "exclude_ended": p.ExcludeEnded, + "exclude_deleted": p.ExcludeDeleted, + "sku_ids": slicehelper.JoinSnowflakes(p.SkuIDs), + } + if p.UserID != 0 { + queryValues["user_id"] = p.UserID + } + if p.Before != 0 { + queryValues["before"] = p.Before + } + if p.After != 0 { + queryValues["after"] = p.After + } + if p.Limit != 0 { + queryValues["limit"] = p.Limit + } + if p.GuildID != 0 { + queryValues["guild_id"] = p.GuildID + } + return queryValues } type applicationsImpl struct { client Client } +func (s *applicationsImpl) GetCurrentApplication(opts ...RequestOpt) (application *discord.Application, err error) { + err = s.client.Do(GetCurrentApplication.Compile(nil), nil, &application, opts...) + return +} + +func (s *applicationsImpl) UpdateCurrentApplication(applicationUpdate discord.ApplicationUpdate, opts ...RequestOpt) (application *discord.Application, err error) { + err = s.client.Do(UpdateCurrentApplication.Compile(nil), applicationUpdate, &application, opts...) + return +} + func (s *applicationsImpl) GetGlobalCommands(applicationID snowflake.ID, withLocalizations bool, opts ...RequestOpt) (commands []discord.ApplicationCommand, err error) { var unmarshalCommands []discord.UnmarshalApplicationCommand err = s.client.Do(GetGlobalCommands.Compile(discord.QueryValues{"with_localizations": withLocalizations}, applicationID), nil, &unmarshalCommands, opts...) @@ -49,7 +113,7 @@ func (s *applicationsImpl) GetGlobalCommands(applicationID snowflake.ID, withLoc func (s *applicationsImpl) GetGlobalCommand(applicationID snowflake.ID, commandID snowflake.ID, opts ...RequestOpt) (command discord.ApplicationCommand, err error) { var unmarshalCommand discord.UnmarshalApplicationCommand - err = s.client.Do(GetGlobalCommand.Compile(nil, applicationID, commandID), nil, &command, opts...) + err = s.client.Do(GetGlobalCommand.Compile(nil, applicationID, commandID), nil, &unmarshalCommand, opts...) if err == nil { command = unmarshalCommand.ApplicationCommand } @@ -58,7 +122,7 @@ func (s *applicationsImpl) GetGlobalCommand(applicationID snowflake.ID, commandI func (s *applicationsImpl) CreateGlobalCommand(applicationID snowflake.ID, commandCreate discord.ApplicationCommandCreate, opts ...RequestOpt) (command discord.ApplicationCommand, err error) { var unmarshalCommand discord.UnmarshalApplicationCommand - err = s.client.Do(CreateGlobalCommand.Compile(nil, applicationID), commandCreate, &command, opts...) + err = s.client.Do(CreateGlobalCommand.Compile(nil, applicationID), commandCreate, &unmarshalCommand, opts...) if err == nil { command = unmarshalCommand.ApplicationCommand } @@ -156,6 +220,62 @@ func (s *applicationsImpl) UpdateApplicationRoleConnectionMetadata(applicationID return } +func (s *applicationsImpl) GetEntitlements(applicationID snowflake.ID, params GetEntitlementsParams, opts ...RequestOpt) (entitlements []discord.Entitlement, err error) { + err = s.client.Do(GetEntitlements.Compile(params.ToQueryValues(), applicationID), nil, &entitlements, opts...) + return +} + +func (s *applicationsImpl) GetEntitlement(applicationID snowflake.ID, entitlementID snowflake.ID, opts ...RequestOpt) (entitlement *discord.Entitlement, err error) { + err = s.client.Do(GetEntitlement.Compile(nil, applicationID, entitlementID), nil, &entitlement, opts...) + return +} + +func (s *applicationsImpl) CreateTestEntitlement(applicationID snowflake.ID, entitlementCreate discord.TestEntitlementCreate, opts ...RequestOpt) (entitlement *discord.Entitlement, err error) { + err = s.client.Do(CreateTestEntitlement.Compile(nil, applicationID), entitlementCreate, &entitlement, opts...) + return +} + +func (s *applicationsImpl) DeleteTestEntitlement(applicationID snowflake.ID, entitlementID snowflake.ID, opts ...RequestOpt) error { + return s.client.Do(DeleteTestEntitlement.Compile(nil, applicationID, entitlementID), nil, nil, opts...) +} + +func (s *applicationsImpl) ConsumeEntitlement(applicationID snowflake.ID, entitlementID snowflake.ID, opts ...RequestOpt) error { + return s.client.Do(ConsumeEntitlement.Compile(nil, applicationID, entitlementID), nil, nil, opts...) +} + +func (s *applicationsImpl) GetApplicationEmojis(applicationID snowflake.ID, opts ...RequestOpt) (emojis []discord.Emoji, err error) { + var rs emojisResponse + err = s.client.Do(GetApplicationEmojis.Compile(nil, applicationID), nil, &rs, opts...) + if err == nil { + emojis = rs.Items + } + return +} + +func (s *applicationsImpl) GetApplicationEmoji(applicationID snowflake.ID, emojiID snowflake.ID, opts ...RequestOpt) (emoji *discord.Emoji, err error) { + err = s.client.Do(GetApplicationEmoji.Compile(nil, applicationID, emojiID), nil, &emoji, opts...) + return +} + +func (s *applicationsImpl) CreateApplicationEmoji(applicationID snowflake.ID, emojiCreate discord.EmojiCreate, opts ...RequestOpt) (emoji *discord.Emoji, err error) { + err = s.client.Do(CreateApplicationEmoji.Compile(nil, applicationID), emojiCreate, &emoji, opts...) + return +} + +func (s *applicationsImpl) UpdateApplicationEmoji(applicationID snowflake.ID, emojiID snowflake.ID, emojiUpdate discord.EmojiUpdate, opts ...RequestOpt) (emoji *discord.Emoji, err error) { + err = s.client.Do(UpdateApplicationEmoji.Compile(nil, applicationID, emojiID), emojiUpdate, &emoji, opts...) + return +} + +func (s *applicationsImpl) DeleteApplicationEmoji(applicationID snowflake.ID, emojiID snowflake.ID, opts ...RequestOpt) error { + return s.client.Do(DeleteApplicationEmoji.Compile(nil, applicationID, emojiID), nil, nil, opts...) +} + +func (s *applicationsImpl) GetActivityInstance(applicationID snowflake.ID, instanceID string, opts ...RequestOpt) (instance *discord.ActivityInstance, err error) { + err = s.client.Do(GetActivityInstance.Compile(nil, applicationID, instanceID), nil, &instance, opts...) + return +} + func unmarshalApplicationCommandsToApplicationCommands(unmarshalCommands []discord.UnmarshalApplicationCommand) []discord.ApplicationCommand { commands := make([]discord.ApplicationCommand, len(unmarshalCommands)) for i := range unmarshalCommands { @@ -163,3 +283,7 @@ func unmarshalApplicationCommandsToApplicationCommands(unmarshalCommands []disco } return commands } + +type emojisResponse struct { + Items []discord.Emoji `json:"items"` +} diff --git a/rest/channels.go b/rest/channels.go index 1ae7dee40..83094eec1 100644 --- a/rest/channels.go +++ b/rest/channels.go @@ -20,8 +20,6 @@ type Channels interface { GetWebhooks(channelID snowflake.ID, opts ...RequestOpt) ([]discord.Webhook, error) CreateWebhook(channelID snowflake.ID, webhookCreate discord.WebhookCreate, opts ...RequestOpt) (*discord.IncomingWebhook, error) - GetPermissionOverwrites(channelID snowflake.ID, opts ...RequestOpt) ([]discord.PermissionOverwrite, error) - GetPermissionOverwrite(channelID snowflake.ID, overwriteID snowflake.ID, opts ...RequestOpt) (*discord.PermissionOverwrite, error) UpdatePermissionOverwrite(channelID snowflake.ID, overwriteID snowflake.ID, permissionOverwrite discord.PermissionOverwriteUpdate, opts ...RequestOpt) error DeletePermissionOverwrite(channelID snowflake.ID, overwriteID snowflake.ID, opts ...RequestOpt) error @@ -36,7 +34,7 @@ type Channels interface { BulkDeleteMessages(channelID snowflake.ID, messageIDs []snowflake.ID, opts ...RequestOpt) error CrosspostMessage(channelID snowflake.ID, messageID snowflake.ID, opts ...RequestOpt) (*discord.Message, error) - GetReactions(channelID snowflake.ID, messageID snowflake.ID, emoji string, opts ...RequestOpt) ([]discord.User, error) + GetReactions(channelID snowflake.ID, messageID snowflake.ID, emoji string, reactionType discord.MessageReactionType, after int, limit int, opts ...RequestOpt) ([]discord.User, error) AddReaction(channelID snowflake.ID, messageID snowflake.ID, emoji string, opts ...RequestOpt) error RemoveOwnReaction(channelID snowflake.ID, messageID snowflake.ID, emoji string, opts ...RequestOpt) error RemoveUserReaction(channelID snowflake.ID, messageID snowflake.ID, emoji string, userID snowflake.ID, opts ...RequestOpt) error @@ -46,7 +44,11 @@ type Channels interface { GetPinnedMessages(channelID snowflake.ID, opts ...RequestOpt) ([]discord.Message, error) PinMessage(channelID snowflake.ID, messageID snowflake.ID, opts ...RequestOpt) error UnpinMessage(channelID snowflake.ID, messageID snowflake.ID, opts ...RequestOpt) error - // TODO: add missing endpoints + Follow(channelID snowflake.ID, targetChannelID snowflake.ID, opts ...RequestOpt) (*discord.FollowedChannel, error) + + GetPollAnswerVotes(channelID snowflake.ID, messageID snowflake.ID, answerID int, after snowflake.ID, limit int, opts ...RequestOpt) ([]discord.User, error) + GetPollAnswerVotesPage(channelID snowflake.ID, messageID snowflake.ID, answerID int, startID snowflake.ID, limit int, opts ...RequestOpt) PollAnswerVotesPage + ExpirePoll(channelID snowflake.ID, messageID snowflake.ID, opts ...RequestOpt) (*discord.Message, error) } type channelImpl struct { @@ -92,16 +94,6 @@ func (s *channelImpl) CreateWebhook(channelID snowflake.ID, webhookCreate discor return } -func (s *channelImpl) GetPermissionOverwrites(channelID snowflake.ID, opts ...RequestOpt) (overwrites []discord.PermissionOverwrite, err error) { - err = s.client.Do(GetPermissionOverwrites.Compile(nil, channelID), nil, &overwrites, opts...) - return -} - -func (s *channelImpl) GetPermissionOverwrite(channelID snowflake.ID, overwriteID snowflake.ID, opts ...RequestOpt) (overwrite *discord.PermissionOverwrite, err error) { - err = s.client.Do(GetPermissionOverwrite.Compile(nil, channelID, overwriteID), nil, &overwrite, opts...) - return -} - func (s *channelImpl) UpdatePermissionOverwrite(channelID snowflake.ID, overwriteID snowflake.ID, permissionOverwrite discord.PermissionOverwriteUpdate, opts ...RequestOpt) error { return s.client.Do(UpdatePermissionOverwrite.Compile(nil, channelID, overwriteID), permissionOverwrite, nil, opts...) } @@ -180,8 +172,17 @@ func (s *channelImpl) CrosspostMessage(channelID snowflake.ID, messageID snowfla return } -func (s *channelImpl) GetReactions(channelID snowflake.ID, messageID snowflake.ID, emoji string, opts ...RequestOpt) (users []discord.User, err error) { - err = s.client.Do(GetReactions.Compile(nil, channelID, messageID, emoji), nil, &users, opts...) +func (s *channelImpl) GetReactions(channelID snowflake.ID, messageID snowflake.ID, emoji string, reactionType discord.MessageReactionType, after int, limit int, opts ...RequestOpt) (users []discord.User, err error) { + values := discord.QueryValues{ + "type": reactionType, + } + if after != 0 { + values["after"] = after + } + if limit != 0 { + values["limit"] = limit + } + err = s.client.Do(GetReactions.Compile(values, channelID, messageID, emoji), nil, &users, opts...) return } @@ -222,3 +223,37 @@ func (s *channelImpl) Follow(channelID snowflake.ID, targetChannelID snowflake.I err = s.client.Do(FollowChannel.Compile(nil, channelID), discord.FollowChannel{ChannelID: targetChannelID}, &followedChannel, opts...) return } + +func (s *channelImpl) GetPollAnswerVotes(channelID snowflake.ID, messageID snowflake.ID, answerID int, after snowflake.ID, limit int, opts ...RequestOpt) (users []discord.User, err error) { + values := discord.QueryValues{} + if after != 0 { + values["after"] = after + } + if limit != 0 { + values["limit"] = limit + } + var rs pollAnswerVotesResponse + err = s.client.Do(GetPollAnswerVotes.Compile(values, channelID, messageID, answerID), nil, &rs, opts...) + if err == nil { + users = rs.Users + } + return +} + +func (s *channelImpl) GetPollAnswerVotesPage(channelID snowflake.ID, messageID snowflake.ID, answerID int, startID snowflake.ID, limit int, opts ...RequestOpt) PollAnswerVotesPage { + return PollAnswerVotesPage{ + getItems: func(after snowflake.ID) ([]discord.User, error) { + return s.GetPollAnswerVotes(channelID, messageID, answerID, after, limit, opts...) + }, + ID: startID, + } +} + +func (s *channelImpl) ExpirePoll(channelID snowflake.ID, messageID snowflake.ID, opts ...RequestOpt) (message *discord.Message, err error) { + err = s.client.Do(ExpirePoll.Compile(nil, channelID, messageID), nil, &message, opts...) + return +} + +type pollAnswerVotesResponse struct { + Users []discord.User `json:"users"` +} diff --git a/rest/emojis.go b/rest/emojis.go index 1b11da60a..789f923f2 100644 --- a/rest/emojis.go +++ b/rest/emojis.go @@ -34,7 +34,7 @@ func (s *emojiImpl) GetEmojis(guildID snowflake.ID, opts ...RequestOpt) (emojis func (s *emojiImpl) GetEmoji(guildID snowflake.ID, emojiID snowflake.ID, opts ...RequestOpt) (emoji *discord.Emoji, err error) { err = s.client.Do(GetEmoji.Compile(nil, guildID, emojiID), nil, &emoji, opts...) - if err != nil { + if emoji != nil { emoji.GuildID = guildID } return @@ -42,7 +42,7 @@ func (s *emojiImpl) GetEmoji(guildID snowflake.ID, emojiID snowflake.ID, opts .. func (s *emojiImpl) CreateEmoji(guildID snowflake.ID, emojiCreate discord.EmojiCreate, opts ...RequestOpt) (emoji *discord.Emoji, err error) { err = s.client.Do(CreateEmoji.Compile(nil, guildID), emojiCreate, &emoji, opts...) - if err != nil { + if emoji != nil { emoji.GuildID = guildID } return @@ -50,7 +50,7 @@ func (s *emojiImpl) CreateEmoji(guildID snowflake.ID, emojiCreate discord.EmojiC func (s *emojiImpl) UpdateEmoji(guildID snowflake.ID, emojiID snowflake.ID, emojiUpdate discord.EmojiUpdate, opts ...RequestOpt) (emoji *discord.Emoji, err error) { err = s.client.Do(UpdateEmoji.Compile(nil, guildID, emojiID), emojiUpdate, &emoji, opts...) - if err != nil { + if emoji != nil { emoji.GuildID = guildID } return diff --git a/rest/guilds.go b/rest/guilds.go index 86211bfec..e4180086b 100644 --- a/rest/guilds.go +++ b/rest/guilds.go @@ -3,6 +3,7 @@ package rest import ( "time" + "github.com/disgoorg/disgo/internal/slicehelper" "github.com/disgoorg/snowflake/v2" "github.com/disgoorg/disgo/discord" @@ -39,6 +40,7 @@ type Guilds interface { GetBan(guildID snowflake.ID, userID snowflake.ID, opts ...RequestOpt) (*discord.Ban, error) AddBan(guildID snowflake.ID, userID snowflake.ID, deleteMessageDuration time.Duration, opts ...RequestOpt) error DeleteBan(guildID snowflake.ID, userID snowflake.ID, opts ...RequestOpt) error + BulkBan(guildID snowflake.ID, ban discord.BulkBan, opts ...RequestOpt) (*discord.BulkBanResult, error) GetIntegrations(guildID snowflake.ID, opts ...RequestOpt) ([]discord.Integration, error) DeleteIntegration(guildID snowflake.ID, integrationID snowflake.ID, opts ...RequestOpt) error @@ -211,6 +213,11 @@ func (s *guildImpl) DeleteBan(guildID snowflake.ID, userID snowflake.ID, opts .. return s.client.Do(DeleteBan.Compile(nil, guildID, userID), nil, nil, opts...) } +func (s *guildImpl) BulkBan(guildID snowflake.ID, ban discord.BulkBan, opts ...RequestOpt) (result *discord.BulkBanResult, err error) { + err = s.client.Do(BulkBan.Compile(nil, guildID), ban, &result, opts...) + return +} + func (s *guildImpl) GetIntegrations(guildID snowflake.ID, opts ...RequestOpt) (integrations []discord.Integration, err error) { err = s.client.Do(GetIntegrations.Compile(nil, guildID), nil, &integrations, opts...) return @@ -222,16 +229,9 @@ func (s *guildImpl) DeleteIntegration(guildID snowflake.ID, integrationID snowfl func (s *guildImpl) GetGuildPruneCount(guildID snowflake.ID, days int, includeRoles []snowflake.ID, opts ...RequestOpt) (result *discord.GuildPruneResult, err error) { values := discord.QueryValues{ - "days": days, - } - var joinedRoles string - for i, roleID := range includeRoles { - joinedRoles += roleID.String() - if i != len(includeRoles)-1 { - joinedRoles += "," - } + "days": days, + "include_roles": slicehelper.JoinSnowflakes(includeRoles), } - values["include_roles"] = joinedRoles err = s.client.Do(GetGuildPruneCount.Compile(values, guildID), nil, &result, opts...) return } diff --git a/rest/interactions.go b/rest/interactions.go index 17747c6f0..9d687c516 100644 --- a/rest/interactions.go +++ b/rest/interactions.go @@ -15,6 +15,7 @@ func NewInteractions(client Client) Interactions { type Interactions interface { GetInteractionResponse(applicationID snowflake.ID, interactionToken string, opts ...RequestOpt) (*discord.Message, error) CreateInteractionResponse(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) error + CreateInteractionResponseWithCallback(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) (*discord.InteractionCallbackResponse, error) UpdateInteractionResponse(applicationID snowflake.ID, interactionToken string, messageUpdate discord.MessageUpdate, opts ...RequestOpt) (*discord.Message, error) DeleteInteractionResponse(applicationID snowflake.ID, interactionToken string, opts ...RequestOpt) error @@ -33,6 +34,8 @@ func (s *interactionImpl) GetInteractionResponse(interactionID snowflake.ID, int return } +// CreateInteractionResponse responds to the interaction without returning the callback. +// If you need the callback, use CreateInteractionResponseWithCallback. func (s *interactionImpl) CreateInteractionResponse(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) error { body, err := interactionResponse.ToBody() if err != nil { @@ -42,6 +45,18 @@ func (s *interactionImpl) CreateInteractionResponse(interactionID snowflake.ID, return s.client.Do(CreateInteractionResponse.Compile(nil, interactionID, interactionToken), body, nil, opts...) } +func (s *interactionImpl) CreateInteractionResponseWithCallback(interactionID snowflake.ID, interactionToken string, interactionResponse discord.InteractionResponse, opts ...RequestOpt) (callback *discord.InteractionCallbackResponse, err error) { + body, err := interactionResponse.ToBody() + if err != nil { + return nil, err + } + values := discord.QueryValues{ + "with_response": true, + } + err = s.client.Do(CreateInteractionResponse.Compile(values, interactionID, interactionToken), body, &callback, opts...) + return +} + func (s *interactionImpl) UpdateInteractionResponse(applicationID snowflake.ID, interactionToken string, messageUpdate discord.MessageUpdate, opts ...RequestOpt) (message *discord.Message, err error) { body, err := messageUpdate.ToBody() if err != nil { diff --git a/rest/invites.go b/rest/invites.go index 5416c455f..20a46c816 100644 --- a/rest/invites.go +++ b/rest/invites.go @@ -16,8 +16,8 @@ type Invites interface { GetInvite(code string, opts ...RequestOpt) (*discord.Invite, error) CreateInvite(channelID snowflake.ID, inviteCreate discord.InviteCreate, opts ...RequestOpt) (*discord.Invite, error) DeleteInvite(code string, opts ...RequestOpt) (*discord.Invite, error) - GetGuildInvites(guildID snowflake.ID, opts ...RequestOpt) ([]discord.Invite, error) - GetChannelInvites(channelID snowflake.ID, opts ...RequestOpt) ([]discord.Invite, error) + GetGuildInvites(guildID snowflake.ID, opts ...RequestOpt) ([]discord.ExtendedInvite, error) + GetChannelInvites(channelID snowflake.ID, opts ...RequestOpt) ([]discord.ExtendedInvite, error) } type inviteImpl struct { @@ -39,12 +39,12 @@ func (s *inviteImpl) DeleteInvite(code string, opts ...RequestOpt) (invite *disc return } -func (s *inviteImpl) GetGuildInvites(guildID snowflake.ID, opts ...RequestOpt) (invites []discord.Invite, err error) { +func (s *inviteImpl) GetGuildInvites(guildID snowflake.ID, opts ...RequestOpt) (invites []discord.ExtendedInvite, err error) { err = s.client.Do(GetGuildInvites.Compile(nil, guildID), nil, &invites, opts...) return } -func (s *inviteImpl) GetChannelInvites(channelID snowflake.ID, opts ...RequestOpt) (invites []discord.Invite, err error) { +func (s *inviteImpl) GetChannelInvites(channelID snowflake.ID, opts ...RequestOpt) (invites []discord.ExtendedInvite, err error) { err = s.client.Do(GetChannelInvites.Compile(nil, channelID), nil, &invites, opts...) return } diff --git a/rest/members.go b/rest/members.go index ccaa6360a..fca986573 100644 --- a/rest/members.go +++ b/rest/members.go @@ -25,6 +25,8 @@ type Members interface { UpdateCurrentMember(guildID snowflake.ID, nick string, opts ...RequestOpt) (*string, error) + GetCurrentUserVoiceState(guildID snowflake.ID, opts ...RequestOpt) (*discord.VoiceState, error) + GetUserVoiceState(guildID snowflake.ID, userID snowflake.ID, opts ...RequestOpt) (*discord.VoiceState, error) UpdateCurrentUserVoiceState(guildID snowflake.ID, currentUserVoiceStateUpdate discord.CurrentUserVoiceStateUpdate, opts ...RequestOpt) error UpdateUserVoiceState(guildID snowflake.ID, userID snowflake.ID, userVoiceStateUpdate discord.UserVoiceStateUpdate, opts ...RequestOpt) error } @@ -105,6 +107,16 @@ func (s *memberImpl) UpdateCurrentMember(guildID snowflake.ID, nick string, opts return } +func (s *memberImpl) GetCurrentUserVoiceState(guildID snowflake.ID, opts ...RequestOpt) (state *discord.VoiceState, err error) { + err = s.client.Do(GetCurrentUserVoiceState.Compile(nil, guildID), nil, &state, opts...) + return +} + +func (s *memberImpl) GetUserVoiceState(guildID snowflake.ID, userID snowflake.ID, opts ...RequestOpt) (state *discord.VoiceState, err error) { + err = s.client.Do(GetUserVoiceState.Compile(nil, guildID, userID), nil, &state, opts...) + return +} + func (s *memberImpl) UpdateCurrentUserVoiceState(guildID snowflake.ID, currentUserVoiceStateUpdate discord.CurrentUserVoiceStateUpdate, opts ...RequestOpt) error { return s.client.Do(UpdateCurrentUserVoiceState.Compile(nil, guildID), currentUserVoiceStateUpdate, nil, opts...) } diff --git a/rest/oauth2.go b/rest/oauth2.go index a09484307..8843edf5d 100644 --- a/rest/oauth2.go +++ b/rest/oauth2.go @@ -1,6 +1,7 @@ package rest import ( + "errors" "net/url" "github.com/disgoorg/snowflake/v2" @@ -8,6 +9,9 @@ import ( "github.com/disgoorg/disgo/discord" ) +// ErrMissingBearerToken is returned when a bearer token is missing for a request which requires it. +var ErrMissingBearerToken = errors.New("missing bearer token") + var _ OAuth2 = (*oAuth2Impl)(nil) func NewOAuth2(client Client) OAuth2 { @@ -18,9 +22,15 @@ type OAuth2 interface { GetBotApplicationInfo(opts ...RequestOpt) (*discord.Application, error) GetCurrentAuthorizationInfo(bearerToken string, opts ...RequestOpt) (*discord.AuthorizationInformation, error) + // GetCurrentUser returns the current user + // Leave bearerToken empty to use the bot token. GetCurrentUser(bearerToken string, opts ...RequestOpt) (*discord.OAuth2User, error) GetCurrentMember(bearerToken string, guildID snowflake.ID, opts ...RequestOpt) (*discord.Member, error) + // GetCurrentUserGuilds returns a list of guilds the current user is a member of. Requires the discord.OAuth2ScopeGuilds scope. + // Leave bearerToken empty to use the bot token. GetCurrentUserGuilds(bearerToken string, before snowflake.ID, after snowflake.ID, limit int, withCounts bool, opts ...RequestOpt) ([]discord.OAuth2Guild, error) + // GetCurrentUserGuildsPage returns a Page of guilds the current user is a member of. Requires the discord.OAuth2ScopeGuilds scope. + // Leave bearerToken empty to use the bot token. GetCurrentUserGuildsPage(bearerToken string, startID snowflake.ID, limit int, withCounts bool, opts ...RequestOpt) Page[discord.OAuth2Guild] GetCurrentUserConnections(bearerToken string, opts ...RequestOpt) ([]discord.Connection, error) @@ -50,6 +60,9 @@ func (s *oAuth2Impl) GetBotApplicationInfo(opts ...RequestOpt) (application *dis } func (s *oAuth2Impl) GetCurrentAuthorizationInfo(bearerToken string, opts ...RequestOpt) (info *discord.AuthorizationInformation, err error) { + if bearerToken == "" { + return nil, ErrMissingBearerToken + } err = s.client.Do(GetAuthorizationInfo.Compile(nil), nil, &info, withBearerToken(bearerToken, opts)...) return } @@ -60,6 +73,9 @@ func (s *oAuth2Impl) GetCurrentUser(bearerToken string, opts ...RequestOpt) (use } func (s *oAuth2Impl) GetCurrentMember(bearerToken string, guildID snowflake.ID, opts ...RequestOpt) (member *discord.Member, err error) { + if bearerToken == "" { + return nil, ErrMissingBearerToken + } err = s.client.Do(GetCurrentMember.Compile(nil, guildID), nil, &member, withBearerToken(bearerToken, opts)...) return } @@ -94,21 +110,33 @@ func (s *oAuth2Impl) GetCurrentUserGuildsPage(bearerToken string, startID snowfl } func (s *oAuth2Impl) GetCurrentUserConnections(bearerToken string, opts ...RequestOpt) (connections []discord.Connection, err error) { + if bearerToken == "" { + return nil, ErrMissingBearerToken + } err = s.client.Do(GetCurrentUserConnections.Compile(nil), nil, &connections, withBearerToken(bearerToken, opts)...) return } func (s *oAuth2Impl) SetGuildCommandPermissions(bearerToken string, applicationID snowflake.ID, guildID snowflake.ID, commandID snowflake.ID, commandPermissions []discord.ApplicationCommandPermission, opts ...RequestOpt) (commandPerms *discord.ApplicationCommandPermissions, err error) { + if bearerToken == "" { + return nil, ErrMissingBearerToken + } err = s.client.Do(SetGuildCommandPermissions.Compile(nil, applicationID, guildID, commandID), discord.ApplicationCommandPermissionsSet{Permissions: commandPermissions}, &commandPerms, withBearerToken(bearerToken, opts)...) return } func (s *oAuth2Impl) GetCurrentUserApplicationRoleConnection(bearerToken string, applicationID snowflake.ID, opts ...RequestOpt) (connection *discord.ApplicationRoleConnection, err error) { + if bearerToken == "" { + return nil, ErrMissingBearerToken + } err = s.client.Do(GetCurrentUserApplicationRoleConnection.Compile(nil, applicationID), nil, &connection, withBearerToken(bearerToken, opts)...) return } func (s *oAuth2Impl) UpdateCurrentUserApplicationRoleConnection(bearerToken string, applicationID snowflake.ID, connectionUpdate discord.ApplicationRoleConnectionUpdate, opts ...RequestOpt) (connection *discord.ApplicationRoleConnection, err error) { + if bearerToken == "" { + return nil, ErrMissingBearerToken + } err = s.client.Do(UpdateCurrentUserApplicationRoleConnection.Compile(nil, applicationID), connectionUpdate, &connection, withBearerToken(bearerToken, opts)...) return } diff --git a/rest/page.go b/rest/page.go index a3d7b76be..6065b1793 100644 --- a/rest/page.go +++ b/rest/page.go @@ -117,3 +117,28 @@ func (p *ThreadMemberPage) Next() bool { } return p.Err == nil } + +type PollAnswerVotesPage struct { + getItems func(after snowflake.ID) ([]discord.User, error) + + Items []discord.User + Err error + + ID snowflake.ID +} + +func (p *PollAnswerVotesPage) Next() bool { + if p.Err != nil { + return false + } + + if len(p.Items) > 0 { + p.ID = p.Items[0].ID + } + + p.Items, p.Err = p.getItems(p.ID) + if p.Err == nil && len(p.Items) == 0 { + p.Err = ErrNoMorePages + } + return p.Err == nil +} diff --git a/rest/query_params.go b/rest/query_params.go new file mode 100644 index 000000000..9d17c83d0 --- /dev/null +++ b/rest/query_params.go @@ -0,0 +1,11 @@ +package rest + +import ( + "github.com/disgoorg/disgo/discord" +) + +// QueryParams serves as a generic interface for implementations of rest endpoint query parameters. +type QueryParams interface { + // ToQueryValues transforms fields from the QueryParams interface implementations into discord.QueryValues. + ToQueryValues() discord.QueryValues +} diff --git a/rest/rest.go b/rest/rest.go index 850b2bf57..aeb09a012 100644 --- a/rest/rest.go +++ b/rest/rest.go @@ -18,9 +18,11 @@ type Rest interface { Users Voice Webhooks + SoundboardSounds StageInstances Emojis Stickers + SKUs GuildScheduledEvents } @@ -44,9 +46,11 @@ func New(client Client) Rest { Users: NewUsers(client), Voice: NewVoice(client), Webhooks: NewWebhooks(client), + SoundboardSounds: NewSoundboardSounds(client), StageInstances: NewStageInstances(client), Emojis: NewEmojis(client), Stickers: NewStickers(client), + SKUs: NewSKUs(client), GuildScheduledEvents: NewGuildScheduledEvents(client), } } @@ -68,8 +72,10 @@ type restImpl struct { Users Voice Webhooks + SoundboardSounds StageInstances Emojis Stickers + SKUs GuildScheduledEvents } diff --git a/rest/rest_client.go b/rest/rest_client.go index 8330178b9..0c3a55fc8 100644 --- a/rest/rest_client.go +++ b/rest/rest_client.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "log/slog" "net/http" "net/url" "time" @@ -18,6 +19,7 @@ import ( func NewClient(botToken string, opts ...ConfigOpt) Client { config := DefaultConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "rest_client")) config.RateLimiter.Reset() @@ -32,7 +34,7 @@ type Client interface { // HTTPClient returns the http.Client the rest client uses HTTPClient() *http.Client - // RateLimiter returns the rrate.RateLimiter the rest client uses + // RateLimiter returns the RateLimiter the rest client uses RateLimiter() RateLimiter // Close closes the rest client and awaits all pending requests to finish. You can use a cancelling context to abort the waiting @@ -83,7 +85,7 @@ func (c *clientImpl) retry(endpoint *CompiledEndpoint, rqBody any, rsBody any, t return fmt.Errorf("failed to marshal request body: %w", err) } } - c.config.Logger.Tracef("request to %s, body: %s", endpoint.URL, string(rawRqBody)) + c.config.Logger.Debug("new request", slog.String("endpoint", endpoint.URL), slog.String("body", string(rawRqBody))) } rq, err := http.NewRequest(endpoint.Endpoint.Method, c.config.URL+endpoint.URL, bytes.NewReader(rawRqBody)) @@ -143,16 +145,15 @@ func (c *clientImpl) retry(endpoint *CompiledEndpoint, rqBody any, rsBody any, t if rawRsBody, err = io.ReadAll(rs.Body); err != nil { return fmt.Errorf("error reading response body in rest client: %w", err) } - c.config.Logger.Tracef("response from %s, code %d, body: %s", endpoint.URL, rs.StatusCode, string(rawRsBody)) + c.config.Logger.Debug("new response", slog.String("endpoint", endpoint.URL), slog.String("code", rs.Status), slog.String("body", string(rawRsBody))) } switch rs.StatusCode { case http.StatusOK, http.StatusCreated, http.StatusNoContent: if rsBody != nil && rs.Body != nil { if err = json.Unmarshal(rawRsBody, rsBody); err != nil { - wErr := fmt.Errorf("error unmarshalling response body: %w", err) - c.config.Logger.Error(wErr) - return wErr + c.config.Logger.Error("error unmarshalling response body", slog.Any("err", err), slog.String("endpoint", endpoint.URL), slog.String("code", rs.Status), slog.String("body", string(rawRsBody))) + return fmt.Errorf("error unmarshalling response body: %w", err) } } return nil diff --git a/rest/rest_config.go b/rest/rest_config.go index 799b49037..7fec8507e 100644 --- a/rest/rest_config.go +++ b/rest/rest_config.go @@ -2,16 +2,15 @@ package rest import ( "fmt" + "log/slog" "net/http" "time" - - "github.com/disgoorg/log" ) // DefaultConfig is the configuration which is used by default func DefaultConfig() *Config { return &Config{ - Logger: log.Default(), + Logger: slog.Default(), HTTPClient: &http.Client{Timeout: 20 * time.Second}, URL: fmt.Sprintf("%sv%d", API, Version), } @@ -19,12 +18,12 @@ func DefaultConfig() *Config { // Config is the configuration for the rest client type Config struct { - Logger log.Logger - HTTPClient *http.Client - RateLimiter RateLimiter - RateRateLimiterConfigOpts []RateLimiterConfigOpt - URL string - UserAgent string + Logger *slog.Logger + HTTPClient *http.Client + RateLimiter RateLimiter + RateLimiterConfigOpts []RateLimiterConfigOpt + URL string + UserAgent string } // ConfigOpt can be used to supply optional parameters to NewClient @@ -36,12 +35,12 @@ func (c *Config) Apply(opts []ConfigOpt) { opt(c) } if c.RateLimiter == nil { - c.RateLimiter = NewRateLimiter(c.RateRateLimiterConfigOpts...) + c.RateLimiter = NewRateLimiter(c.RateLimiterConfigOpts...) } } // WithLogger applies a custom logger to the rest rate limiter -func WithLogger(logger log.Logger) ConfigOpt { +func WithLogger(logger *slog.Logger) ConfigOpt { return func(config *Config) { config.Logger = logger } @@ -54,17 +53,17 @@ func WithHTTPClient(httpClient *http.Client) ConfigOpt { } } -// WithRateLimiter applies a custom rrate.RateLimiter to the rest client +// WithRateLimiter applies a custom RateLimiter to the rest client func WithRateLimiter(rateLimiter RateLimiter) ConfigOpt { return func(config *Config) { config.RateLimiter = rateLimiter } } -// WithRateRateLimiterConfigOpts applies rrate.ConfigOpt for the rrate.RateLimiter to the rest rate limiter -func WithRateRateLimiterConfigOpts(opts ...RateLimiterConfigOpt) ConfigOpt { +// WithRateLimiterConfigOpts applies RateLimiterConfigOpt to the RateLimiter +func WithRateLimiterConfigOpts(opts ...RateLimiterConfigOpt) ConfigOpt { return func(config *Config) { - config.RateRateLimiterConfigOpts = append(config.RateRateLimiterConfigOpts, opts...) + config.RateLimiterConfigOpts = append(config.RateLimiterConfigOpts, opts...) } } diff --git a/rest/rest_endpoints.go b/rest/rest_endpoints.go index 375dacb2d..8b8194bb7 100644 --- a/rest/rest_endpoints.go +++ b/rest/rest_endpoints.go @@ -29,7 +29,7 @@ var ( // OAuth2 var ( GetBotApplicationInfo = NewEndpoint(http.MethodGet, "/oauth2/applications/@me") - GetAuthorizationInfo = NewEndpoint(http.MethodGet, "/oauth2/@me") + GetAuthorizationInfo = NewNoBotAuthEndpoint(http.MethodGet, "/oauth2/@me") Token = NewEndpoint(http.MethodPost, "/oauth2/token") ) @@ -37,14 +37,13 @@ var ( var ( GetUser = NewEndpoint(http.MethodGet, "/users/{user.id}") GetCurrentUser = NewEndpoint(http.MethodGet, "/users/@me") - GetCurrentMember = NewEndpoint(http.MethodGet, "/users/@me/guilds/{guild.id}/member") - UpdateSelfUser = NewEndpoint(http.MethodPatch, "/users/@me") + UpdateCurrentUser = NewEndpoint(http.MethodPatch, "/users/@me") + GetCurrentUserGuilds = NewEndpoint(http.MethodGet, "/users/@me/guilds") + GetCurrentMember = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/guilds/{guild.id}/member") GetCurrentUserConnections = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/connections") - GetCurrentUserGuilds = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/guilds") GetCurrentUserApplicationRoleConnection = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/applications/{application.id}/role-connection") UpdateCurrentUserApplicationRoleConnection = NewNoBotAuthEndpoint(http.MethodPut, "/users/@me/applications/{application.id}/role-connection") LeaveGuild = NewEndpoint(http.MethodDelete, "/users/@me/guilds/{guild.id}") - GetDMChannels = NewEndpoint(http.MethodGet, "/users/@me/channels") CreateDMChannel = NewEndpoint(http.MethodPost, "/users/@me/channels") ) @@ -65,6 +64,7 @@ var ( GetBan = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/bans/{user.id}") AddBan = NewEndpoint(http.MethodPut, "/guilds/{guild.id}/bans/{user.id}") DeleteBan = NewEndpoint(http.MethodDelete, "/guilds/{guild.id}/bans/{user.id}") + BulkBan = NewEndpoint(http.MethodPost, "/guilds/{guild.id}/bulk-ban") GetMember = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/members/{user.id}") GetMembers = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/members") @@ -94,6 +94,8 @@ var ( UpdateGuildIncidentActions = NewEndpoint(http.MethodPut, "/guilds/{guild.id}/incident-actions") + GetCurrentUserVoiceState = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/voice-states/@me") + GetUserVoiceState = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/voice-states/{user.id}") UpdateCurrentUserVoiceState = NewEndpoint(http.MethodPatch, "/guilds/{guild.id}/voice-states/@me") UpdateUserVoiceState = NewEndpoint(http.MethodPatch, "/guilds/{guild.id}/voice-states/{user.id}") ) @@ -138,6 +140,16 @@ var ( GetGuildScheduledEventUsers = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}/users") ) +// Sounds +var ( + GetSoundboardDefaultSounds = NewEndpoint(http.MethodGet, "/soundboard-default-sounds") + GetGuildSoundboardSounds = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/soundboard-sounds") + CreateGuildSoundboardSound = NewEndpoint(http.MethodPost, "/guilds/{guild.id}/soundboard-sounds") + GetGuildSoundboardSound = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/soundboard-sounds/{sound.id}") + UpdateGuildSoundboardSound = NewEndpoint(http.MethodPatch, "/guilds/{guild.id}/soundboard-sounds/{sound.id}") + DeleteGuildSoundboardSound = NewEndpoint(http.MethodDelete, "/guilds/{guild.id}/soundboard-sounds/{sound.id}") +) + // StageInstance var ( GetStageInstance = NewEndpoint(http.MethodGet, "/stage-instances/{channel.id}") @@ -165,13 +177,16 @@ var ( GetChannelWebhooks = NewEndpoint(http.MethodGet, "/channels/{channel.id}/webhooks") CreateWebhook = NewEndpoint(http.MethodPost, "/channels/{channel.id}/webhooks") - GetPermissionOverwrites = NewEndpoint(http.MethodGet, "/channels/{channel.id}/permissions") - GetPermissionOverwrite = NewEndpoint(http.MethodGet, "/channels/{channel.id}/permissions/{overwrite.id}") UpdatePermissionOverwrite = NewEndpoint(http.MethodPut, "/channels/{channel.id}/permissions/{overwrite.id}") DeletePermissionOverwrite = NewEndpoint(http.MethodDelete, "/channels/{channel.id}/permissions/{overwrite.id}") SendTyping = NewEndpoint(http.MethodPost, "/channels/{channel.id}/typing") FollowChannel = NewEndpoint(http.MethodPost, "/channels/{channel.id}/followers") + + GetPollAnswerVotes = NewEndpoint(http.MethodGet, "/channels/{channel.id}/polls/{message.id}/answers/{answer.id}") + ExpirePoll = NewEndpoint(http.MethodPost, "/channels/{channel.id}/polls/{message.id}/expire") + + SendSoundboardSound = NewEndpoint(http.MethodPost, "/channels/{channel.id}/send-soundboard-sound") ) // Threads @@ -188,6 +203,7 @@ var ( GetPublicArchivedThreads = NewEndpoint(http.MethodGet, "/channels/{channel.id}/threads/archived/public") GetPrivateArchivedThreads = NewEndpoint(http.MethodGet, "/channels/{channel.id}/threads/archived/private") GetJoinedPrivateArchivedThreads = NewEndpoint(http.MethodGet, "/channels/{channel.id}/users/@me/threads/archived/private") + GetActiveGuildThreads = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/threads/active") ) // Messages @@ -225,6 +241,7 @@ var ( // Stickers var ( GetNitroStickerPacks = NewEndpoint(http.MethodGet, "/sticker-packs") + GetNitroStickerPack = NewEndpoint(http.MethodGet, "/sticker-packs/{pack.id}") GetSticker = NewEndpoint(http.MethodGet, "/stickers/{sticker.id}") GetGuildStickers = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/stickers") CreateGuildSticker = NewEndpoint(http.MethodPost, "/guilds/{guild.id}/stickers") @@ -261,6 +278,9 @@ var ( // Applications var ( + GetCurrentApplication = NewEndpoint(http.MethodGet, "/applications/@me") + UpdateCurrentApplication = NewEndpoint(http.MethodPatch, "/applications/@me") + GetGlobalCommands = NewEndpoint(http.MethodGet, "/applications/{application.id}/commands") GetGlobalCommand = NewEndpoint(http.MethodGet, "/applications/{application.id}/command/{command.id}") CreateGlobalCommand = NewEndpoint(http.MethodPost, "/applications/{application.id}/commands") @@ -277,7 +297,7 @@ var ( GetGuildCommandsPermissions = NewEndpoint(http.MethodGet, "/applications/{application.id}/guilds/{guild.id}/commands/permissions") GetGuildCommandPermissions = NewEndpoint(http.MethodGet, "/applications/{application.id}/guilds/{guild.id}/commands/{command.id}/permissions") - SetGuildCommandPermissions = NewEndpoint(http.MethodPut, "/applications/{application.id}/guilds/{guild.id}/commands/{command.id}/permissions") + SetGuildCommandPermissions = NewNoBotAuthEndpoint(http.MethodPut, "/applications/{application.id}/guilds/{guild.id}/commands/{command.id}/permissions") GetInteractionResponse = NewNoBotAuthEndpoint(http.MethodGet, "/webhooks/{application.id}/{interaction.token}/messages/@original") CreateInteractionResponse = NewNoBotAuthEndpoint(http.MethodPost, "/interactions/{interaction.id}/{interaction.token}/callback") @@ -291,6 +311,28 @@ var ( GetApplicationRoleConnectionMetadata = NewEndpoint(http.MethodGet, "/applications/{application.id}/role-connections/metadata") UpdateApplicationRoleConnectionMetadata = NewEndpoint(http.MethodPut, "/applications/{application.id}/role-connections/metadata") + + GetEntitlements = NewEndpoint(http.MethodGet, "/applications/{application.id}/entitlements") + GetEntitlement = NewEndpoint(http.MethodGet, "/applications/{application.id}/entitlements/{entitlement.id}") + CreateTestEntitlement = NewEndpoint(http.MethodPost, "/applications/{application.id}/entitlements") + DeleteTestEntitlement = NewEndpoint(http.MethodDelete, "/applications/{application.id}/entitlements/{entitlement.id}") + ConsumeEntitlement = NewEndpoint(http.MethodPost, "/applications/{application.id}/entitlements/{entitlement.id}/consume") + + GetApplicationEmojis = NewEndpoint(http.MethodGet, "/applications/{application.id}/emojis") + GetApplicationEmoji = NewEndpoint(http.MethodGet, "/applications/{application.id}/emojis/{emoji.id}") + CreateApplicationEmoji = NewEndpoint(http.MethodPost, "/applications/{application.id}/emojis") + UpdateApplicationEmoji = NewEndpoint(http.MethodPatch, "/applications/{application.id}/emojis/{emoji.id}") + DeleteApplicationEmoji = NewEndpoint(http.MethodDelete, "/applications/{application.id}/emojis/{emoji.id}") + + GetActivityInstance = NewEndpoint(http.MethodGet, "/applications/{application.id}/activity-instances/{instance.id}") +) + +// SKUs +var ( + GetSKUs = NewEndpoint(http.MethodGet, "/applications/{application.id}/skus") + + GetSKUSubscriptions = NewEndpoint(http.MethodGet, "/skus/{sku.id}/subscriptions") + GetSKUSubscription = NewEndpoint(http.MethodGet, "/skus/{sku.id}/subscriptions/{subscription.id}") ) // NewEndpoint returns a new Endpoint which requires bot auth with the given http method & route. diff --git a/rest/rest_rate_limiter.go b/rest/rest_rate_limiter.go index d9be6977a..c056bb534 100644 --- a/rest/rest_rate_limiter.go +++ b/rest/rest_rate_limiter.go @@ -3,6 +3,7 @@ package rest import ( "context" "fmt" + "log/slog" "net/http" "strconv" "sync" @@ -11,6 +12,13 @@ import ( "github.com/sasha-s/go-csync" ) +const ( + // MaxRetries is the maximum number of retries the client should do + MaxRetries = 10 + // CleanupInterval is the interval at which the rate limiter cleans up old buckets + CleanupInterval = time.Second * 10 +) + // RateLimiter can be used to supply your own rate limit implementation type RateLimiter interface { // MaxRetries returns the maximum number of retries the client should do @@ -34,6 +42,7 @@ type RateLimiter interface { func NewRateLimiter(opts ...RateLimiterConfigOpt) RateLimiter { config := DefaultRateLimiterConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "rest_rate_limiter")) rateLimiter := &rateLimiterImpl{ config: *config, @@ -83,13 +92,13 @@ func (l *rateLimiterImpl) doCleanup() { continue } if b.Reset.Before(now) { - l.config.Logger.Debugf("cleaning up bucket, Hash: %s, ID: %s, Reset: %s", hash, b.ID, b.Reset) + l.config.Logger.Debug("cleaning up bucket", slog.String("hash", hash), slog.String("id", b.ID), slog.Time("reset", b.Reset)) delete(l.buckets, hash) } b.mu.Unlock() } if before != len(l.buckets) { - l.config.Logger.Debugf("cleaned up %d rate limit buckets", before-len(l.buckets)) + l.config.Logger.Debug("cleaned up rate limit buckets", slog.Int("before", before), slog.Int("after", len(l.buckets)), slog.Int("removed", before-len(l.buckets))) } } @@ -132,10 +141,10 @@ func (l *rateLimiterImpl) getRouteHash(endpoint *CompiledEndpoint) string { func (l *rateLimiterImpl) getBucket(endpoint *CompiledEndpoint, create bool) *bucket { hash := l.getRouteHash(endpoint) - l.config.Logger.Trace("locking buckets") + l.config.Logger.Debug("locking buckets") l.bucketsMu.Lock() defer func() { - l.config.Logger.Trace("unlocking buckets") + l.config.Logger.Debug("unlocking buckets") l.bucketsMu.Unlock() }() b, ok := l.buckets[hash] @@ -156,7 +165,7 @@ func (l *rateLimiterImpl) getBucket(endpoint *CompiledEndpoint, create bool) *bu func (l *rateLimiterImpl) WaitBucket(ctx context.Context, endpoint *CompiledEndpoint) error { b := l.getBucket(endpoint, true) - l.config.Logger.Tracef("locking rest bucket, ID: %s, Limit: %d, Remaining: %d, Reset: %s", b.ID, b.Limit, b.Remaining, b.Reset) + l.config.Logger.Debug("locking rest bucket", slog.String("id", b.ID), slog.Int("limit", b.Limit), slog.Int("remaining", b.Remaining), slog.Time("reset", b.Reset)) if err := b.mu.CLock(ctx); err != nil { return err } @@ -192,7 +201,7 @@ func (l *rateLimiterImpl) UnlockBucket(endpoint *CompiledEndpoint, rs *http.Resp return nil } defer func() { - l.config.Logger.Tracef("unlocking rest bucket, ID: %s, Limit: %d, Remaining: %d, Reset: %s", b.ID, b.Limit, b.Remaining, b.Reset) + l.config.Logger.Debug("unlocking rest bucket", slog.String("id", b.ID), slog.Int("limit", b.Limit), slog.Int("remaining", b.Remaining), slog.Time("reset", b.Reset)) b.mu.Unlock() }() @@ -217,7 +226,7 @@ func (l *rateLimiterImpl) UnlockBucket(endpoint *CompiledEndpoint, rs *http.Resp resetAfterHeader := rs.Header.Get("X-RateLimit-Reset-After") retryAfterHeader := rs.Header.Get("Retry-After") - l.config.Logger.Tracef("code: %d, headers: global %t, cloudflare: %t, remaining: %s, limit: %s, reset: %s, retryAfter: %s", rs.StatusCode, global, cloudflare, remainingHeader, limitHeader, resetHeader, retryAfterHeader) + l.config.Logger.Debug("ratelimit response headers", slog.Int("code", rs.StatusCode), slog.Bool("global", global), slog.Bool("cloudflare", cloudflare), slog.String("remaining", remainingHeader), slog.String("limit", limitHeader), slog.String("reset", resetHeader), slog.String("reset_after", resetAfterHeader), slog.String("retry_after", retryAfterHeader)) // we hit a rate limit. let's see if it was global cloudflare or a route specific one if rs.StatusCode == http.StatusTooManyRequests { @@ -228,14 +237,14 @@ func (l *rateLimiterImpl) UnlockBucket(endpoint *CompiledEndpoint, rs *http.Resp reset := time.Now().Add(time.Second * time.Duration(retryAfter)) if global { l.global = reset - l.config.Logger.Warnf("global rate limit exceeded, retry after: %ds", retryAfter) + l.config.Logger.Warn("global rate limit exceeded", slog.Int("retry_after", retryAfter)) } else if cloudflare { l.global = reset - l.config.Logger.Warnf("cloudflare rate limit exceeded, retry after: %ds", retryAfter) + l.config.Logger.Warn("cloudflare rate limit exceeded", slog.Int("retry_after", retryAfter)) } else { b.Remaining = 0 b.Reset = reset - l.config.Logger.Warnf("rate limit on route %s exceeded, retry after: %ds", endpoint.URL, retryAfter) + l.config.Logger.Warn("rate limit exceeded", slog.String("endpoint", endpoint.URL), slog.Int("retry_after", retryAfter)) } return nil } @@ -243,7 +252,7 @@ func (l *rateLimiterImpl) UnlockBucket(endpoint *CompiledEndpoint, rs *http.Resp if limitHeader != "" { limit, err := strconv.Atoi(limitHeader) if err != nil { - return fmt.Errorf("invalid limit %s: %s", limitHeader, err) + return fmt.Errorf("invalid limit %s: %w", limitHeader, err) } b.Limit = limit } @@ -251,7 +260,7 @@ func (l *rateLimiterImpl) UnlockBucket(endpoint *CompiledEndpoint, rs *http.Resp if remainingHeader != "" { remaining, err := strconv.Atoi(remainingHeader) if err != nil { - return fmt.Errorf("invalid remaining %s: %s", remainingHeader, err) + return fmt.Errorf("invalid remaining %s: %w", remainingHeader, err) } b.Remaining = remaining } @@ -260,14 +269,14 @@ func (l *rateLimiterImpl) UnlockBucket(endpoint *CompiledEndpoint, rs *http.Resp if resetAfterHeader != "" { resetAfter, err := strconv.ParseFloat(resetAfterHeader, 64) if err != nil { - return fmt.Errorf("invalid reset after %s: %s", resetAfterHeader, err) + return fmt.Errorf("invalid reset after %s: %w", resetAfterHeader, err) } b.Reset = time.Now().Add(time.Duration(resetAfter) * time.Second) } else if resetHeader != "" { reset, err := strconv.ParseFloat(resetHeader, 64) if err != nil { - return fmt.Errorf("invalid reset %s: %s", resetHeader, err) + return fmt.Errorf("invalid reset %s: %w", resetHeader, err) } sec := int64(reset) diff --git a/rest/rest_rate_limiter_config.go b/rest/rest_rate_limiter_config.go index bd36f261c..b829a27c6 100644 --- a/rest/rest_rate_limiter_config.go +++ b/rest/rest_rate_limiter_config.go @@ -1,23 +1,22 @@ package rest import ( + "log/slog" "time" - - "github.com/disgoorg/log" ) // DefaultRateLimiterConfig is the configuration which is used by default. func DefaultRateLimiterConfig() *RateLimiterConfig { return &RateLimiterConfig{ - Logger: log.Default(), - MaxRetries: 10, - CleanupInterval: time.Second * 10, + Logger: slog.Default(), + MaxRetries: MaxRetries, + CleanupInterval: CleanupInterval, } } // RateLimiterConfig is the configuration for the rate limiter. type RateLimiterConfig struct { - Logger log.Logger + Logger *slog.Logger MaxRetries int CleanupInterval time.Duration } @@ -33,7 +32,7 @@ func (c *RateLimiterConfig) Apply(opts []RateLimiterConfigOpt) { } // WithRateLimiterLogger applies a custom logger to the rest rate limiter. -func WithRateLimiterLogger(logger log.Logger) RateLimiterConfigOpt { +func WithRateLimiterLogger(logger *slog.Logger) RateLimiterConfigOpt { return func(config *RateLimiterConfig) { config.Logger = logger } diff --git a/rest/skus.go b/rest/skus.go new file mode 100644 index 000000000..f81a38c14 --- /dev/null +++ b/rest/skus.go @@ -0,0 +1,64 @@ +package rest + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" +) + +var _ SKUs = (*skusImpl)(nil) + +func NewSKUs(client Client) SKUs { + return &skusImpl{client: client} +} + +type SKUs interface { + GetSKUs(applicationID snowflake.ID, opts ...RequestOpt) ([]discord.SKU, error) + + GetSKUSubscriptions(skuID snowflake.ID, before snowflake.ID, after snowflake.ID, limit int, userID snowflake.ID, opts ...RequestOpt) ([]discord.Subscription, error) + GetSKUSubscriptionsPage(skuID snowflake.ID, userID snowflake.ID, startID snowflake.ID, limit int, opts ...RequestOpt) Page[discord.Subscription] + GetSKUSubscription(skuID snowflake.ID, subscriptionID snowflake.ID, opts ...RequestOpt) (*discord.Subscription, error) +} + +type skusImpl struct { + client Client +} + +func (s *skusImpl) GetSKUs(applicationID snowflake.ID, opts ...RequestOpt) (skus []discord.SKU, err error) { + err = s.client.Do(GetSKUs.Compile(nil, applicationID), nil, &skus, opts...) + return +} + +func (s *skusImpl) GetSKUSubscriptions(skuID snowflake.ID, before snowflake.ID, after snowflake.ID, limit int, userID snowflake.ID, opts ...RequestOpt) (subscriptions []discord.Subscription, err error) { + values := discord.QueryValues{} + if before != 0 { + values["before"] = before + } + if after != 0 { + values["after"] = after + } + if limit != 0 { + values["limit"] = limit + } + if userID != 0 { + values["user_id"] = userID + } + err = s.client.Do(GetSKUSubscriptions.Compile(values, skuID), nil, &subscriptions, opts...) + return +} + +func (s *skusImpl) GetSKUSubscriptionsPage(skuID snowflake.ID, userID snowflake.ID, startID snowflake.ID, limit int, opts ...RequestOpt) Page[discord.Subscription] { + return Page[discord.Subscription]{ + getItemsFunc: func(before snowflake.ID, after snowflake.ID) ([]discord.Subscription, error) { + return s.GetSKUSubscriptions(skuID, before, after, limit, userID, opts...) + }, + getIDFunc: func(subscription discord.Subscription) snowflake.ID { + return subscription.ID + }, + ID: startID, + } +} + +func (s *skusImpl) GetSKUSubscription(skuID snowflake.ID, subscriptionID snowflake.ID, opts ...RequestOpt) (subscription *discord.Subscription, err error) { + err = s.client.Do(GetSKUSubscription.Compile(nil, skuID, subscriptionID), nil, &subscription, opts...) + return +} diff --git a/rest/soundboard_sounds.go b/rest/soundboard_sounds.go new file mode 100644 index 000000000..7886e6b68 --- /dev/null +++ b/rest/soundboard_sounds.go @@ -0,0 +1,67 @@ +package rest + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" +) + +var _ SoundboardSounds = (*soundsImpl)(nil) + +func NewSoundboardSounds(client Client) SoundboardSounds { + return &soundsImpl{client: client} +} + +type SoundboardSounds interface { + GetSoundboardDefaultSounds(opts ...RequestOpt) ([]discord.SoundboardSound, error) + GetGuildSoundboardSounds(guildID snowflake.ID, opts ...RequestOpt) ([]discord.SoundboardSound, error) + CreateGuildSoundboardSound(guildID snowflake.ID, soundCreate discord.SoundboardSoundCreate, opts ...RequestOpt) (*discord.SoundboardSound, error) + GetGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) (*discord.SoundboardSound, error) + UpdateGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, soundUpdate discord.SoundboardSoundUpdate, opts ...RequestOpt) (*discord.SoundboardSound, error) + DeleteGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) error + SendSoundboardSound(channelID snowflake.ID, sendSound discord.SendSoundboardSound, opts ...RequestOpt) error +} + +type soundsImpl struct { + client Client +} + +func (s *soundsImpl) GetSoundboardDefaultSounds(opts ...RequestOpt) (sounds []discord.SoundboardSound, err error) { + err = s.client.Do(GetSoundboardDefaultSounds.Compile(nil), nil, &sounds, opts...) + return +} + +func (s *soundsImpl) GetGuildSoundboardSounds(guildID snowflake.ID, opts ...RequestOpt) (sounds []discord.SoundboardSound, err error) { + var rs soundsResponse + err = s.client.Do(GetGuildSoundboardSounds.Compile(nil, guildID), nil, &rs, opts...) + if err == nil { + sounds = rs.Items + } + return +} + +func (s *soundsImpl) CreateGuildSoundboardSound(guildID snowflake.ID, soundCreate discord.SoundboardSoundCreate, opts ...RequestOpt) (sound *discord.SoundboardSound, err error) { + err = s.client.Do(CreateGuildSoundboardSound.Compile(nil, guildID), soundCreate, &sound, opts...) + return +} + +func (s *soundsImpl) GetGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) (sound *discord.SoundboardSound, err error) { + err = s.client.Do(GetGuildSoundboardSound.Compile(nil, guildID, soundID), nil, &sound, opts...) + return +} + +func (s *soundsImpl) UpdateGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, soundUpdate discord.SoundboardSoundUpdate, opts ...RequestOpt) (sound *discord.SoundboardSound, err error) { + err = s.client.Do(UpdateGuildSoundboardSound.Compile(nil, guildID, soundID), soundUpdate, &sound, opts...) + return +} + +func (s *soundsImpl) DeleteGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) error { + return s.client.Do(DeleteGuildSoundboardSound.Compile(nil, guildID, soundID), nil, nil, opts...) +} + +func (s *soundsImpl) SendSoundboardSound(channelID snowflake.ID, sendSound discord.SendSoundboardSound, opts ...RequestOpt) error { + return s.client.Do(SendSoundboardSound.Compile(nil, channelID), sendSound, nil, opts...) +} + +type soundsResponse struct { + Items []discord.SoundboardSound `json:"items"` +} diff --git a/rest/stickers.go b/rest/stickers.go index 45090c9b4..694ab0f8c 100644 --- a/rest/stickers.go +++ b/rest/stickers.go @@ -14,6 +14,7 @@ func NewStickers(client Client) Stickers { type Stickers interface { GetNitroStickerPacks(opts ...RequestOpt) ([]discord.StickerPack, error) + GetNitroStickerPack(packID snowflake.ID, opts ...RequestOpt) (*discord.StickerPack, error) GetSticker(stickerID snowflake.ID, opts ...RequestOpt) (*discord.Sticker, error) GetStickers(guildID snowflake.ID, opts ...RequestOpt) ([]discord.Sticker, error) CreateSticker(guildID snowflake.ID, createSticker discord.StickerCreate, opts ...RequestOpt) (*discord.Sticker, error) @@ -34,6 +35,11 @@ func (s *stickerImpl) GetNitroStickerPacks(opts ...RequestOpt) (stickerPacks []d return } +func (s *stickerImpl) GetNitroStickerPack(packID snowflake.ID, opts ...RequestOpt) (pack *discord.StickerPack, err error) { + err = s.client.Do(GetNitroStickerPack.Compile(nil, packID), nil, &pack, opts...) + return +} + func (s *stickerImpl) GetSticker(stickerID snowflake.ID, opts ...RequestOpt) (sticker *discord.Sticker, err error) { err = s.client.Do(GetSticker.Compile(nil, stickerID), nil, &sticker, opts...) return diff --git a/rest/threads.go b/rest/threads.go index 573141570..eddfb78c4 100644 --- a/rest/threads.go +++ b/rest/threads.go @@ -30,6 +30,7 @@ type Threads interface { GetPublicArchivedThreads(channelID snowflake.ID, before time.Time, limit int, opts ...RequestOpt) (threads *discord.GetThreads, err error) GetPrivateArchivedThreads(channelID snowflake.ID, before time.Time, limit int, opts ...RequestOpt) (threads *discord.GetThreads, err error) GetJoinedPrivateArchivedThreads(channelID snowflake.ID, before time.Time, limit int, opts ...RequestOpt) (threads *discord.GetThreads, err error) + GetActiveGuildThreads(guildID snowflake.ID, opts ...RequestOpt) (*discord.GuildActiveThreads, error) } type threadImpl struct { @@ -133,6 +134,11 @@ func (s *threadImpl) GetJoinedPrivateArchivedThreads(channelID snowflake.ID, bef return } +func (s *threadImpl) GetActiveGuildThreads(guildID snowflake.ID, opts ...RequestOpt) (activeThreads *discord.GuildActiveThreads, err error) { + err = s.client.Do(GetActiveGuildThreads.Compile(nil, guildID), nil, &activeThreads, opts...) + return +} + func (s *threadImpl) getThreadMembers(threadID snowflake.ID, queryValues discord.QueryValues, opts ...RequestOpt) (threadMembers []discord.ThreadMember, err error) { err = s.client.Do(GetThreadMembers.Compile(queryValues, threadID), nil, &threadMembers, opts...) return diff --git a/rest/users.go b/rest/users.go index ac15783d4..2f9f69cee 100644 --- a/rest/users.go +++ b/rest/users.go @@ -14,10 +14,8 @@ func NewUsers(client Client) Users { type Users interface { GetUser(userID snowflake.ID, opts ...RequestOpt) (*discord.User, error) - UpdateSelfUser(selfUserUpdate discord.SelfUserUpdate, opts ...RequestOpt) (*discord.OAuth2User, error) - GetGuilds(before int, after int, limit int, withCounts bool, opts ...RequestOpt) ([]discord.OAuth2Guild, error) + UpdateCurrentUser(userUpdate discord.UserUpdate, opts ...RequestOpt) (*discord.OAuth2User, error) LeaveGuild(guildID snowflake.ID, opts ...RequestOpt) error - GetDMChannels(opts ...RequestOpt) ([]discord.Channel, error) CreateDMChannel(userID snowflake.ID, opts ...RequestOpt) (*discord.DMChannel, error) } @@ -30,25 +28,8 @@ func (s *userImpl) GetUser(userID snowflake.ID, opts ...RequestOpt) (user *disco return } -func (s *userImpl) UpdateSelfUser(updateSelfUser discord.SelfUserUpdate, opts ...RequestOpt) (selfUser *discord.OAuth2User, err error) { - err = s.client.Do(UpdateSelfUser.Compile(nil), updateSelfUser, &selfUser, opts...) - return -} - -func (s *userImpl) GetGuilds(before int, after int, limit int, withCounts bool, opts ...RequestOpt) (guilds []discord.OAuth2Guild, err error) { - queryParams := discord.QueryValues{ - "with_counts": withCounts, - } - if before > 0 { - queryParams["before"] = before - } - if after > 0 { - queryParams["after"] = after - } - if limit > 0 { - queryParams["limit"] = limit - } - err = s.client.Do(GetCurrentUserGuilds.Compile(queryParams), nil, &guilds, opts...) +func (s *userImpl) UpdateCurrentUser(userUpdate discord.UserUpdate, opts ...RequestOpt) (selfUser *discord.OAuth2User, err error) { + err = s.client.Do(UpdateCurrentUser.Compile(nil), userUpdate, &selfUser, opts...) return } @@ -56,11 +37,6 @@ func (s *userImpl) LeaveGuild(guildID snowflake.ID, opts ...RequestOpt) error { return s.client.Do(LeaveGuild.Compile(nil, guildID), nil, nil, opts...) } -func (s *userImpl) GetDMChannels(opts ...RequestOpt) (channels []discord.Channel, err error) { - err = s.client.Do(GetDMChannels.Compile(nil), nil, &channels, opts...) - return -} - func (s *userImpl) CreateDMChannel(userID snowflake.ID, opts ...RequestOpt) (channel *discord.DMChannel, err error) { err = s.client.Do(CreateDMChannel.Compile(nil), discord.DMChannelCreate{RecipientID: userID}, &channel, opts...) return diff --git a/rest/webhooks.go b/rest/webhooks.go index a6be6fea9..f4cfcb9bc 100644 --- a/rest/webhooks.go +++ b/rest/webhooks.go @@ -50,9 +50,8 @@ func (s *webhookImpl) UpdateWebhook(webhookID snowflake.ID, webhookUpdate discor return } -func (s *webhookImpl) DeleteWebhook(webhookID snowflake.ID, opts ...RequestOpt) (err error) { - err = s.client.Do(DeleteWebhook.Compile(nil, webhookID), nil, nil, opts...) - return +func (s *webhookImpl) DeleteWebhook(webhookID snowflake.ID, opts ...RequestOpt) error { + return s.client.Do(DeleteWebhook.Compile(nil, webhookID), nil, nil, opts...) } func (s *webhookImpl) GetWebhookWithToken(webhookID snowflake.ID, webhookToken string, opts ...RequestOpt) (webhook discord.Webhook, err error) { @@ -73,9 +72,8 @@ func (s *webhookImpl) UpdateWebhookWithToken(webhookID snowflake.ID, webhookToke return } -func (s *webhookImpl) DeleteWebhookWithToken(webhookID snowflake.ID, webhookToken string, opts ...RequestOpt) (err error) { - err = s.client.Do(DeleteWebhookWithToken.Compile(nil, webhookID, webhookToken), nil, nil, opts...) - return +func (s *webhookImpl) DeleteWebhookWithToken(webhookID snowflake.ID, webhookToken string, opts ...RequestOpt) error { + return s.client.Do(DeleteWebhookWithToken.Compile(nil, webhookID, webhookToken), nil, nil, opts...) } func (s *webhookImpl) createWebhookMessage(webhookID snowflake.ID, webhookToken string, messageCreate discord.Payload, wait bool, threadID snowflake.ID, endpoint *Endpoint, opts []RequestOpt) (message *discord.Message, err error) { @@ -127,11 +125,10 @@ func (s *webhookImpl) UpdateWebhookMessage(webhookID snowflake.ID, webhookToken return } -func (s *webhookImpl) DeleteWebhookMessage(webhookID snowflake.ID, webhookToken string, messageID snowflake.ID, threadID snowflake.ID, opts ...RequestOpt) (err error) { +func (s *webhookImpl) DeleteWebhookMessage(webhookID snowflake.ID, webhookToken string, messageID snowflake.ID, threadID snowflake.ID, opts ...RequestOpt) error { params := discord.QueryValues{} if threadID != 0 { params["thread_id"] = threadID } - err = s.client.Do(DeleteWebhookMessage.Compile(params, webhookID, webhookToken, messageID), nil, nil, opts...) - return + return s.client.Do(DeleteWebhookMessage.Compile(params, webhookID, webhookToken, messageID), nil, nil, opts...) } diff --git a/sharding/shard_manager.go b/sharding/shard_manager.go index d41af9bc6..236303751 100644 --- a/sharding/shard_manager.go +++ b/sharding/shard_manager.go @@ -8,6 +8,9 @@ import ( "github.com/disgoorg/disgo/gateway" ) +// ShardSplitCount is the default count a shard should be split into when it needs re-sharding. +const ShardSplitCount = 2 + // ShardManager manages multiple gateway.Gateway connections. // For more information on sharding see: https://discord.com/developers/docs/topics/gateway#sharding type ShardManager interface { diff --git a/sharding/shard_manager_config.go b/sharding/shard_manager_config.go index bb2ecb3d4..a78dc3605 100644 --- a/sharding/shard_manager_config.go +++ b/sharding/shard_manager_config.go @@ -1,7 +1,7 @@ package sharding import ( - "github.com/disgoorg/log" + "log/slog" "github.com/disgoorg/disgo/gateway" ) @@ -9,16 +9,16 @@ import ( // DefaultConfig returns a Config with sensible defaults. func DefaultConfig() *Config { return &Config{ - Logger: log.Default(), + Logger: slog.Default(), GatewayCreateFunc: gateway.New, - ShardSplitCount: 2, + ShardSplitCount: ShardSplitCount, } } // Config lets you configure your ShardManager instance. type Config struct { // Logger is the logger of the ShardManager. Defaults to log.Default() - Logger log.Logger + Logger *slog.Logger // ShardIDs is a map of shardIDs the ShardManager should manage. Leave this nil to manage all shards. ShardIDs map[int]struct{} // ShardCount is the total shard count of the ShardManager. Leave this at 0 to let Discord calculate the shard count for you. @@ -33,8 +33,8 @@ type Config struct { GatewayConfigOpts []gateway.ConfigOpt // RateLimiter is the RateLimiter which is used by the ShardManager. Defaults to NewRateLimiter() RateLimiter RateLimiter - // RateRateLimiterConfigOpts are the RateLimiterConfigOpt(s) which are applied to the RateLimiter. - RateRateLimiterConfigOpts []RateLimiterConfigOpt + // RateLimiterConfigOpts are the RateLimiterConfigOpt(s) which are applied to the RateLimiter. + RateLimiterConfigOpts []RateLimiterConfigOpt } // ConfigOpt is a type alias for a function that takes a Config and is used to configure your Server. @@ -46,12 +46,12 @@ func (c *Config) Apply(opts []ConfigOpt) { opt(c) } if c.RateLimiter == nil { - c.RateLimiter = NewRateLimiter(c.RateRateLimiterConfigOpts...) + c.RateLimiter = NewRateLimiter(c.RateLimiterConfigOpts...) } } // WithLogger sets the logger of the ShardManager. -func WithLogger(logger log.Logger) ConfigOpt { +func WithLogger(logger *slog.Logger) ConfigOpt { return func(config *Config) { config.Logger = logger } @@ -105,16 +105,16 @@ func WithGatewayConfigOpts(opts ...gateway.ConfigOpt) ConfigOpt { } } -// WithRateLimiter lets you inject your own srate.RateLimiter into the ShardManager. +// WithRateLimiter lets you inject your own RateLimiter into the ShardManager. func WithRateLimiter(rateLimiter RateLimiter) ConfigOpt { return func(config *Config) { config.RateLimiter = rateLimiter } } -// WithRateRateLimiterConfigOpt lets you configure the default srate.RateLimiter used by the ShardManager. -func WithRateRateLimiterConfigOpt(opts ...RateLimiterConfigOpt) ConfigOpt { +// WithRateLimiterConfigOpt lets you configure the default RateLimiter used by the ShardManager. +func WithRateLimiterConfigOpt(opts ...RateLimiterConfigOpt) ConfigOpt { return func(config *Config) { - config.RateRateLimiterConfigOpts = append(config.RateRateLimiterConfigOpts, opts...) + config.RateLimiterConfigOpts = append(config.RateLimiterConfigOpts, opts...) } } diff --git a/sharding/shard_manager_impl.go b/sharding/shard_manager_impl.go index e26c0139c..f3002c44b 100644 --- a/sharding/shard_manager_impl.go +++ b/sharding/shard_manager_impl.go @@ -3,6 +3,8 @@ package sharding import ( "context" "errors" + "fmt" + "log/slog" "sync" "github.com/disgoorg/snowflake/v2" @@ -17,6 +19,7 @@ var _ ShardManager = (*shardManagerImpl)(nil) func New(token string, eventHandlerFunc gateway.EventHandlerFunc, opts ...ConfigOpt) ShardManager { config := DefaultConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "sharding")) return &shardManagerImpl{ shards: map[int]gateway.Gateway{}, @@ -40,7 +43,7 @@ func (m *shardManagerImpl) closeHandler(shard gateway.Gateway, err error) { if !m.config.AutoScaling || !errors.As(err, &closeError) || gateway.CloseEventCodeByCode(closeError.Code) != gateway.CloseEventCodeShardingRequired { return } - m.config.Logger.Debugf("shard %d requires re-sharding", shard.ShardID()) + m.config.Logger.Debug("shard requires re-sharding", slog.Int("shardID", shard.ShardID())) // make sure shard is closed shard.Close(context.TODO()) @@ -51,7 +54,6 @@ func (m *shardManagerImpl) closeHandler(shard gateway.Gateway, err error) { delete(m.config.ShardIDs, shard.ShardID()) newShardCount := shard.ShardCount() * m.config.ShardSplitCount - if newShardCount > m.config.ShardCount { m.config.ShardCount = newShardCount } @@ -70,7 +72,7 @@ func (m *shardManagerImpl) closeHandler(shard gateway.Gateway, err error) { go func() { defer wg.Done() if err := m.config.RateLimiter.WaitBucket(context.TODO(), shardID); err != nil { - m.config.Logger.Errorf("failed to wait shard bucket %d: %s", shardID, err) + m.config.Logger.Error("failed to wait shard bucket", slog.Any("err", err), slog.Int("shard_id", shardID)) return } defer m.config.RateLimiter.UnlockBucket(shardID) @@ -78,16 +80,16 @@ func (m *shardManagerImpl) closeHandler(shard gateway.Gateway, err error) { newShard := m.config.GatewayCreateFunc(m.token, m.eventHandlerFunc, m.closeHandler, append(m.config.GatewayConfigOpts, gateway.WithShardID(shardID), gateway.WithShardCount(newShardCount))...) m.shards[shardID] = newShard if err := newShard.Open(context.TODO()); err != nil { - m.config.Logger.Errorf("failed to re shard %d, error: %s", shardID, err) + m.config.Logger.Error("failed to re shard", slog.Any("err", err), slog.Int("shard_id", shardID)) } }() } wg.Wait() - m.config.Logger.Debugf("re-sharded shard %d into newShards: %d, newShardCount: %d", shard.ShardID(), newShardIDs, newShardCount) + m.config.Logger.Debug("re-sharded shard", slog.Int("shard_id", shard.ShardID()), slog.String("new_shard_ids", fmt.Sprint(newShardIDs)), slog.Int("new_shard_count", newShardCount)) } func (m *shardManagerImpl) Open(ctx context.Context) { - m.config.Logger.Debugf("opening %+v shards...", m.config.ShardIDs) + m.config.Logger.Debug("opening shards", slog.String("shard_ids", fmt.Sprint(m.config.ShardIDs))) var wg sync.WaitGroup m.shardsMu.Lock() @@ -102,7 +104,7 @@ func (m *shardManagerImpl) Open(ctx context.Context) { go func() { defer wg.Done() if err := m.config.RateLimiter.WaitBucket(ctx, shardID); err != nil { - m.config.Logger.Errorf("failed to wait shard bucket %d: %s", shardID, err) + m.config.Logger.Error("failed to wait shard bucket", slog.Any("err", err), slog.Int("shard_id", shardID)) return } defer m.config.RateLimiter.UnlockBucket(shardID) @@ -110,7 +112,7 @@ func (m *shardManagerImpl) Open(ctx context.Context) { shard := m.config.GatewayCreateFunc(m.token, m.eventHandlerFunc, m.closeHandler, append(m.config.GatewayConfigOpts, gateway.WithShardID(shardID), gateway.WithShardCount(m.config.ShardCount))...) m.shards[shardID] = shard if err := shard.Open(ctx); err != nil { - m.config.Logger.Errorf("failed to open shard %d: %s", shardID, err) + m.config.Logger.Error("failed to open shard", slog.Any("err", err), slog.Int("shard_id", shardID)) } }() } @@ -118,7 +120,7 @@ func (m *shardManagerImpl) Open(ctx context.Context) { } func (m *shardManagerImpl) Close(ctx context.Context) { - m.config.Logger.Debugf("closing %v shards...", m.config.ShardIDs) + m.config.Logger.Debug("closing shards", slog.String("shard_ids", fmt.Sprint(m.config.ShardIDs))) var wg sync.WaitGroup m.shardsMu.Lock() @@ -140,7 +142,7 @@ func (m *shardManagerImpl) OpenShard(ctx context.Context, shardID int) error { } func (m *shardManagerImpl) openShard(ctx context.Context, shardID int, shardCount int) error { - m.config.Logger.Debugf("opening shard %d...", shardID) + m.config.Logger.Debug("opening shard", slog.Int("shard_id", shardID)) if err := m.config.RateLimiter.WaitBucket(ctx, shardID); err != nil { return err @@ -156,7 +158,7 @@ func (m *shardManagerImpl) openShard(ctx context.Context, shardID int, shardCoun } func (m *shardManagerImpl) CloseShard(ctx context.Context, shardID int) { - m.config.Logger.Debugf("closing shard %d...", shardID) + m.config.Logger.Debug("closing shard", slog.Int("shard_id", shardID)) m.shardsMu.Lock() defer m.shardsMu.Unlock() shard, ok := m.shards[shardID] diff --git a/sharding/shard_rate_limiter.go b/sharding/shard_rate_limiter.go index 613cc0513..da21a281e 100644 --- a/sharding/shard_rate_limiter.go +++ b/sharding/shard_rate_limiter.go @@ -4,6 +4,9 @@ import ( "context" ) +// MaxConcurrency is the default number of shards that can log in at the same time. +const MaxConcurrency = 1 + // RateLimiter limits how many shards can log in to Discord at the same time. type RateLimiter interface { // Close gracefully closes the RateLimiter. diff --git a/sharding/shard_rate_limiter_config.go b/sharding/shard_rate_limiter_config.go index 7193d7191..eb5eaf22b 100644 --- a/sharding/shard_rate_limiter_config.go +++ b/sharding/shard_rate_limiter_config.go @@ -1,20 +1,20 @@ package sharding import ( - "github.com/disgoorg/log" + "log/slog" ) // DefaultRateLimiterConfig returns a RateLimiterConfig with sensible defaults. func DefaultRateLimiterConfig() *RateLimiterConfig { return &RateLimiterConfig{ - Logger: log.Default(), - MaxConcurrency: 1, + Logger: slog.Default(), + MaxConcurrency: MaxConcurrency, } } // RateLimiterConfig lets you configure your RateLimiter instance. type RateLimiterConfig struct { - Logger log.Logger + Logger *slog.Logger MaxConcurrency int } @@ -29,7 +29,7 @@ func (c *RateLimiterConfig) Apply(opts []RateLimiterConfigOpt) { } // WithRateLimiterLogger sets the logger for the RateLimiter. -func WithRateLimiterLogger(logger log.Logger) RateLimiterConfigOpt { +func WithRateLimiterLogger(logger *slog.Logger) RateLimiterConfigOpt { return func(config *RateLimiterConfig) { config.Logger = logger } diff --git a/sharding/shard_rate_limiter_impl.go b/sharding/shard_rate_limiter_impl.go index ed4305275..afc0c3beb 100644 --- a/sharding/shard_rate_limiter_impl.go +++ b/sharding/shard_rate_limiter_impl.go @@ -2,6 +2,7 @@ package sharding import ( "context" + "log/slog" "sync" "time" @@ -14,6 +15,7 @@ var _ RateLimiter = (*rateLimiterImpl)(nil) func NewRateLimiter(opts ...RateLimiterConfigOpt) RateLimiter { config := DefaultRateLimiterConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "sharding_rate_limiter")) return &rateLimiterImpl{ buckets: map[int]*bucket{}, @@ -38,7 +40,7 @@ func (r *rateLimiterImpl) Close(ctx context.Context) { go func() { defer wg.Done() if err := b.mu.CLock(ctx); err != nil { - r.config.Logger.Error("failed to close bucket: ", err) + r.config.Logger.Error("failed to close bucket", slog.Any("err", err)) } b.mu.Unlock() }() @@ -46,10 +48,10 @@ func (r *rateLimiterImpl) Close(ctx context.Context) { } func (r *rateLimiterImpl) getBucket(shardID int, create bool) *bucket { - r.config.Logger.Debug("locking shard srate limiter") + r.config.Logger.Debug("locking shard rate limiter") r.mu.Lock() defer func() { - r.config.Logger.Debug("unlocking shard srate limiter") + r.config.Logger.Debug("unlocking shard rate limiter") r.mu.Unlock() }() key := ShardMaxConcurrencyKey(shardID, r.config.MaxConcurrency) @@ -69,7 +71,7 @@ func (r *rateLimiterImpl) getBucket(shardID int, create bool) *bucket { func (r *rateLimiterImpl) WaitBucket(ctx context.Context, shardID int) error { b := r.getBucket(shardID, true) - r.config.Logger.Debugf("locking shard bucket: Key: %d, Reset: %s", b.Key, b.Reset) + r.config.Logger.Debug("locking shard bucket", slog.Int("key", b.Key), slog.Time("reset", b.Reset)) if err := b.mu.CLock(ctx); err != nil { return err } @@ -102,7 +104,7 @@ func (r *rateLimiterImpl) UnlockBucket(shardID int) { return } defer func() { - r.config.Logger.Debugf("unlocking shard bucket: Key: %d, Reset: %s", b.Key, b.Reset) + r.config.Logger.Debug("unlocking shard bucket", slog.Int("key", b.Key), slog.Time("reset", b.Reset)) b.mu.Unlock() }() diff --git a/voice/audio_receiver.go b/voice/audio_receiver.go index 57cad5ab1..868835977 100644 --- a/voice/audio_receiver.go +++ b/voice/audio_receiver.go @@ -3,15 +3,15 @@ package voice import ( "context" "errors" + "log/slog" "net" - "github.com/disgoorg/log" "github.com/disgoorg/snowflake/v2" ) type ( // AudioReceiverCreateFunc is used to create a new AudioReceiver reading audio from the given Conn. - AudioReceiverCreateFunc func(logger log.Logger, receiver OpusFrameReceiver, connection Conn) AudioReceiver + AudioReceiverCreateFunc func(logger *slog.Logger, receiver OpusFrameReceiver, connection Conn) AudioReceiver // UserFilterFunc is used as a filter for which users to receive audio from. UserFilterFunc func(userID snowflake.ID) bool @@ -42,7 +42,7 @@ type ( ) // NewAudioReceiver creates a new AudioReceiver reading audio to the given OpusFrameReceiver from the given Conn. -func NewAudioReceiver(logger log.Logger, opusReceiver OpusFrameReceiver, conn Conn) AudioReceiver { +func NewAudioReceiver(logger *slog.Logger, opusReceiver OpusFrameReceiver, conn Conn) AudioReceiver { return &defaultAudioReceiver{ logger: logger, opusReceiver: opusReceiver, @@ -51,7 +51,7 @@ func NewAudioReceiver(logger log.Logger, opusReceiver OpusFrameReceiver, conn Co } type defaultAudioReceiver struct { - logger log.Logger + logger *slog.Logger cancelFunc context.CancelFunc opusReceiver OpusFrameReceiver conn Conn @@ -62,7 +62,7 @@ func (s *defaultAudioReceiver) Open() { } func (s *defaultAudioReceiver) open() { - defer s.logger.Debugf("closing audio receiver") + defer s.logger.Debug("closing audio receiver") ctx, cancel := context.WithCancel(context.Background()) s.cancelFunc = cancel defer cancel() @@ -88,12 +88,12 @@ func (s *defaultAudioReceiver) receive() { return } if err != nil { - s.logger.Errorf("error while reading packet: %s", err) + s.logger.Error("error while reading packet", slog.Any("err", err)) return } if s.opusReceiver != nil { if err = s.opusReceiver.ReceiveOpusFrame(s.conn.UserIDBySSRC(packet.SSRC), packet); err != nil { - s.logger.Errorf("error while receiving opus frame: %s", err) + s.logger.Error("error while receiving opus frame", slog.Any("err", err)) } } diff --git a/voice/audio_sender.go b/voice/audio_sender.go index b7276e30c..9417b4d18 100644 --- a/voice/audio_sender.go +++ b/voice/audio_sender.go @@ -4,10 +4,9 @@ import ( "context" "errors" "io" + "log/slog" "net" "time" - - "github.com/disgoorg/log" ) // SilenceAudioFrame is a 20ms opus frame of silence. @@ -26,7 +25,7 @@ const ( type ( // AudioSenderCreateFunc is used to create a new AudioSender sending audio to the given Conn. - AudioSenderCreateFunc func(logger log.Logger, provider OpusFrameProvider, conn Conn) AudioSender + AudioSenderCreateFunc func(logger *slog.Logger, provider OpusFrameProvider, conn Conn) AudioSender // AudioSender is used to send audio to a Conn. AudioSender interface { @@ -45,7 +44,7 @@ type ( ) // NewAudioSender creates a new AudioSender sending audio from the given OpusFrameProvider to the given Conn. -func NewAudioSender(logger log.Logger, opusProvider OpusFrameProvider, conn Conn) AudioSender { +func NewAudioSender(logger *slog.Logger, opusProvider OpusFrameProvider, conn Conn) AudioSender { return &defaultAudioSender{ logger: logger, opusProvider: opusProvider, @@ -55,7 +54,7 @@ func NewAudioSender(logger log.Logger, opusProvider OpusFrameProvider, conn Conn } type defaultAudioSender struct { - logger log.Logger + logger *slog.Logger cancelFunc context.CancelFunc opusProvider OpusFrameProvider conn Conn @@ -102,7 +101,7 @@ func (s *defaultAudioSender) send() { } opus, err := s.opusProvider.ProvideOpusFrame() if err != nil && err != io.EOF { - s.logger.Errorf("error while reading opus frame: %s", err) + s.logger.Error("error while reading opus frame", slog.Any("err", err)) return } if len(opus) == 0 { @@ -144,7 +143,7 @@ func (s *defaultAudioSender) handleErr(err error) { s.Close() return } - s.logger.Errorf("failed to send audio: %s", err) + s.logger.Error("failed to send audio", slog.Any("err", err)) } func (s *defaultAudioSender) Close() { diff --git a/voice/conn.go b/voice/conn.go index b90bd9845..368352e77 100644 --- a/voice/conn.go +++ b/voice/conn.go @@ -2,6 +2,7 @@ package voice import ( "context" + "log/slog" "sync" "time" @@ -61,6 +62,7 @@ type ( func NewConn(guildID snowflake.ID, userID snowflake.ID, voiceStateUpdateFunc StateUpdateFunc, removeConnFunc func(), opts ...ConnConfigOpt) Conn { config := DefaultConnConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "voice_conn")) conn := &connImpl{ config: *config, @@ -188,7 +190,7 @@ func (c *connImpl) HandleVoiceServerUpdate(update botgateway.EventVoiceServerUpd ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := c.gateway.Open(ctx, c.state); err != nil { - c.config.Logger.Error("error opening voice gateway. error: ", err) + c.config.Logger.Error("error opening voice gateway", slog.Any("err", err)) } }() } @@ -200,7 +202,7 @@ func (c *connImpl) handleMessage(op Opcode, data GatewayMessageData) { defer cancel() ourAddress, ourPort, err := c.udp.Open(ctx, d.IP, d.Port, d.SSRC) if err != nil { - c.config.Logger.Error("voice: failed to open voiceudp conn. error: ", err) + c.config.Logger.Error("voice: failed to open voiceudp conn", slog.Any("err", err)) break } if err = c.Gateway().Send(ctx, OpcodeSelectProtocol, GatewayMessageDataSelectProtocol{ @@ -211,7 +213,7 @@ func (c *connImpl) handleMessage(op Opcode, data GatewayMessageData) { Mode: EncryptionModeNormal, }, }); err != nil { - c.config.Logger.Error("voice: failed to send select protocol. error: ", err) + c.config.Logger.Error("voice: failed to send select protocol", slog.Any("err", err)) } case GatewayMessageDataSessionDescription: diff --git a/voice/conn_config.go b/voice/conn_config.go index 4480fd396..9f55b802f 100644 --- a/voice/conn_config.go +++ b/voice/conn_config.go @@ -1,13 +1,13 @@ package voice import ( - "github.com/disgoorg/log" + "log/slog" ) // DefaultConnConfig returns a ConnConfig with sensible defaults. func DefaultConnConfig() *ConnConfig { return &ConnConfig{ - Logger: log.Default(), + Logger: slog.Default(), GatewayCreateFunc: NewGateway, UDPConnCreateFunc: NewUDPConn, AudioSenderCreateFunc: NewAudioSender, @@ -17,7 +17,7 @@ func DefaultConnConfig() *ConnConfig { // ConnConfig is used to configure a Conn. type ConnConfig struct { - Logger log.Logger + Logger *slog.Logger GatewayCreateFunc GatewayCreateFunc GatewayConfigOpts []GatewayConfigOpt @@ -42,7 +42,7 @@ func (c *ConnConfig) Apply(opts []ConnConfigOpt) { } // WithConnLogger sets the Conn(s) used Logger. -func WithConnLogger(logger log.Logger) ConnConfigOpt { +func WithConnLogger(logger *slog.Logger) ConnConfigOpt { return func(config *ConnConfig) { config.Logger = logger } diff --git a/voice/gateway.go b/voice/gateway.go index 5e4849224..ecd879717 100644 --- a/voice/gateway.go +++ b/voice/gateway.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log/slog" "sync" "syscall" "time" @@ -92,6 +93,7 @@ type Gateway interface { func NewGateway(eventHandlerFunc EventHandlerFunc, closeHandlerFunc CloseHandlerFunc, opts ...GatewayConfigOpt) Gateway { config := DefaultGatewayConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "voice_conn_gateway")) return &gatewayImpl{ config: *config, @@ -135,13 +137,13 @@ func (g *gatewayImpl) Open(ctx context.Context, state State) error { g.status = StatusConnecting gatewayURL := fmt.Sprintf("wss://%s?v=%d", state.Endpoint, GatewayVersion) - g.config.Logger.Debugf("connecting to voice gateway at: %s", gatewayURL) + g.config.Logger.Debug("connecting to voice gateway at", slog.String("url", gatewayURL)) g.lastHeartbeatSent = time.Now().UTC() conn, rs, err := g.config.Dialer.DialContext(ctx, gatewayURL, nil) if err != nil { g.Close() defer rs.Body.Close() - return fmt.Errorf("error connecting to voice gateway. err: %w", err) + return fmt.Errorf("error connecting to voice gateway: %w", err) } conn.SetCloseHandler(func(code int, text string) error { @@ -161,7 +163,7 @@ func (g *gatewayImpl) Close() { func (g *gatewayImpl) CloseWithCode(code int, message string) { if g.heartbeatTicker != nil { - g.config.Logger.Debug("closing heartbeat goroutines...") + g.config.Logger.Debug("closing heartbeat goroutines") g.heartbeatTicker.Stop() g.heartbeatTicker = nil } @@ -169,9 +171,9 @@ func (g *gatewayImpl) CloseWithCode(code int, message string) { g.connMu.Lock() defer g.connMu.Unlock() if g.conn != nil { - g.config.Logger.Debugf("closing voice gateway connection with code: %d, message: %s", code, message) + g.config.Logger.Debug("closing voice gateway connection", slog.Int("code", code), slog.String("message", message)) if err := g.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(code, message)); err != nil && !errors.Is(err, websocket.ErrCloseSent) { - g.config.Logger.Debug("error writing close code. error: ", err) + g.config.Logger.Debug("error writing close code", slog.Any("err", err)) } _ = g.conn.Close() g.conn = nil @@ -186,7 +188,7 @@ func (g *gatewayImpl) CloseWithCode(code int, message string) { func (g *gatewayImpl) heartbeat() { g.heartbeatTicker = time.NewTicker(g.heartbeatInterval) defer g.heartbeatTicker.Stop() - defer g.config.Logger.Debug("exiting voice heartbeat goroutine...") + defer g.config.Logger.Debug("exiting voice heartbeat goroutine") for range g.heartbeatTicker.C { g.sendHeartbeat() @@ -202,7 +204,7 @@ func (g *gatewayImpl) sendHeartbeat() { if !errors.Is(err, ErrGatewayNotConnected) || errors.Is(err, syscall.EPIPE) { return } - g.config.Logger.Error("failed to send heartbeat. error: ", err) + g.config.Logger.Error("failed to send heartbeat", slog.Any("err", err)) g.CloseWithCode(websocket.CloseServiceRestart, "heartbeat timeout") go g.reconnect() return @@ -211,7 +213,7 @@ func (g *gatewayImpl) sendHeartbeat() { } func (g *gatewayImpl) listen(conn *websocket.Conn) { - defer g.config.Logger.Debug("exiting listen goroutine...") + defer g.config.Logger.Debug("exiting listen goroutine") loop: for { _, reader, err := conn.NextReader() @@ -242,7 +244,7 @@ loop: message, err := g.parseMessage(reader) if err != nil { - g.config.Logger.Error("error while parsing voice gateway event. error: ", err) + g.config.Logger.Error("error while parsing voice gateway event", slog.Any("err", err)) continue } @@ -264,7 +266,7 @@ loop: }) } else { g.status = StatusResuming - err = g.Send(ctx, OpcodeIdentify, GatewayMessageDataResume{ + err = g.Send(ctx, OpcodeResume, GatewayMessageDataResume{ GuildID: g.state.GuildID, SessionID: g.state.SessionID, Token: g.state.Token, @@ -283,7 +285,7 @@ loop: case GatewayMessageDataHeartbeatACK: if int64(d) != g.lastNonce { - g.config.Logger.Errorf("received heartbeat ack with nonce: %d, expected nonce: %d", int64(d), g.lastNonce) + g.config.Logger.Error("received heartbeat ack with nonce", slog.Int64("nonce", int64(d)), slog.Int64("last_nonce", g.lastNonce)) go g.reconnect() break loop } @@ -315,7 +317,7 @@ func (g *gatewayImpl) send(ctx context.Context, messageType int, data []byte) er return ErrGatewayNotConnected } - g.config.Logger.Trace("sending message to voice gateway. data: ", string(data)) + g.config.Logger.Debug("sending message to voice gateway", slog.String("data", string(data))) deadline, ok := ctx.Deadline() if ok { if err := g.conn.SetWriteDeadline(deadline); err != nil { @@ -343,12 +345,12 @@ func (g *gatewayImpl) reconnectTry(ctx context.Context, try int) error { case <-timer.C: } - g.config.Logger.Debug("reconnecting voice gateway...") + g.config.Logger.Debug("reconnecting voice gateway") if err := g.Open(ctx, g.state); err != nil { if errors.Is(err, discord.ErrGatewayAlreadyConnected) { return err } - g.config.Logger.Error("failed to reconnect voice gateway. error: ", err) + g.config.Logger.Error("failed to reconnect voice gateway", slog.Any("err", err)) g.status = StatusDisconnected return g.reconnectTry(ctx, try+1) } @@ -357,14 +359,14 @@ func (g *gatewayImpl) reconnectTry(ctx context.Context, try int) error { func (g *gatewayImpl) reconnect() { if err := g.reconnectTry(context.Background(), 0); err != nil { - g.config.Logger.Error("failed to reopen voice gateway", err) + g.config.Logger.Error("failed to reopen voice gateway", slog.Any("err", err)) } } func (g *gatewayImpl) parseMessage(r io.Reader) (GatewayMessage, error) { buff := &bytes.Buffer{} data, _ := io.ReadAll(io.TeeReader(r, buff)) - g.config.Logger.Tracef("received message from voice gateway. data: %s", string(data)) + g.config.Logger.Debug("received message from voice gateway", slog.String("data", string(data))) var message GatewayMessage if err := json.NewDecoder(buff).Decode(&message); err != nil { diff --git a/voice/gateway_config.go b/voice/gateway_config.go index 53d847007..392b33f3c 100644 --- a/voice/gateway_config.go +++ b/voice/gateway_config.go @@ -1,14 +1,15 @@ package voice import ( - "github.com/disgoorg/log" + "log/slog" + "github.com/gorilla/websocket" ) // DefaultGatewayConfig returns a GatewayConfig with sensible defaults. func DefaultGatewayConfig() *GatewayConfig { return &GatewayConfig{ - Logger: log.Default(), + Logger: slog.Default(), Dialer: websocket.DefaultDialer, AutoReconnect: true, } @@ -16,7 +17,7 @@ func DefaultGatewayConfig() *GatewayConfig { // GatewayConfig is used to configure a Gateway. type GatewayConfig struct { - Logger log.Logger + Logger *slog.Logger Dialer *websocket.Dialer AutoReconnect bool } @@ -32,7 +33,7 @@ func (c *GatewayConfig) Apply(opts []GatewayConfigOpt) { } // WithGatewayLogger sets the Gateway(s) used Logger. -func WithGatewayLogger(logger log.Logger) GatewayConfigOpt { +func WithGatewayLogger(logger *slog.Logger) GatewayConfigOpt { return func(config *GatewayConfig) { config.Logger = logger } diff --git a/voice/manager.go b/voice/manager.go index 5eefebcf0..2f6ab66c1 100644 --- a/voice/manager.go +++ b/voice/manager.go @@ -2,6 +2,7 @@ package voice import ( "context" + "log/slog" "sync" "github.com/disgoorg/snowflake/v2" @@ -42,6 +43,8 @@ type ( func NewManager(voiceStateUpdateFunc StateUpdateFunc, userID snowflake.ID, opts ...ManagerConfigOpt) Manager { config := DefaultManagerConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "voice")) + return &managerImpl{ config: *config, voiceStateUpdateFunc: voiceStateUpdateFunc, @@ -60,7 +63,7 @@ type managerImpl struct { } func (m *managerImpl) HandleVoiceStateUpdate(update gateway.EventVoiceStateUpdate) { - m.config.Logger.Debugf("VoiceStateUpdate for guild: %s", update.GuildID) + m.config.Logger.Debug("new VoiceStateUpdate", slog.Int64("guild_id", int64(update.GuildID))) conn := m.GetConn(update.GuildID) if conn == nil { @@ -70,7 +73,7 @@ func (m *managerImpl) HandleVoiceStateUpdate(update gateway.EventVoiceStateUpdat } func (m *managerImpl) HandleVoiceServerUpdate(update gateway.EventVoiceServerUpdate) { - m.config.Logger.Debugf("VoiceServerUpdate for guild: %s", update.GuildID) + m.config.Logger.Debug("new VoiceServerUpdate", slog.Int64("guild_id", int64(update.GuildID))) conn := m.GetConn(update.GuildID) if conn == nil { @@ -80,7 +83,7 @@ func (m *managerImpl) HandleVoiceServerUpdate(update gateway.EventVoiceServerUpd } func (m *managerImpl) CreateConn(guildID snowflake.ID) Conn { - m.config.Logger.Debugf("Creating new voice conn for guild: %s", guildID) + m.config.Logger.Debug("Creating new voice conn", slog.Int64("guild_id", int64(guildID))) if conn := m.GetConn(guildID); conn != nil { return conn } @@ -112,7 +115,7 @@ func (m *managerImpl) ForEachCon(f func(connection Conn)) { } func (m *managerImpl) RemoveConn(guildID snowflake.ID) { - m.config.Logger.Debugf("Removing voice conn for guild: %s", guildID) + m.config.Logger.Debug("Removing voice conn", slog.Int64("guild_id", int64(guildID))) conn := m.GetConn(guildID) if conn == nil { return diff --git a/voice/manager_config.go b/voice/manager_config.go index 17009c664..b0e9f1a8d 100644 --- a/voice/manager_config.go +++ b/voice/manager_config.go @@ -1,18 +1,18 @@ package voice -import "github.com/disgoorg/log" +import "log/slog" // DefaultManagerConfig returns the default ManagerConfig with sensible defaults. func DefaultManagerConfig() *ManagerConfig { return &ManagerConfig{ - Logger: log.Default(), + Logger: slog.Default(), ConnCreateFunc: NewConn, } } // ManagerConfig is a function that configures a Manager. type ManagerConfig struct { - Logger log.Logger + Logger *slog.Logger ConnCreateFunc ConnCreateFunc ConnOpts []ConnConfigOpt @@ -29,7 +29,7 @@ func (c *ManagerConfig) Apply(opts []ManagerConfigOpt) { } // WithLogger sets the logger for the webhook client -func WithLogger(logger log.Logger) ManagerConfigOpt { +func WithLogger(logger *slog.Logger) ManagerConfigOpt { return func(ManagerConfig *ManagerConfig) { ManagerConfig.Logger = logger } diff --git a/voice/udp_conn.go b/voice/udp_conn.go index eb050111d..a9783cde1 100644 --- a/voice/udp_conn.go +++ b/voice/udp_conn.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "strconv" "strings" @@ -15,8 +16,13 @@ import ( "golang.org/x/crypto/nacl/secretbox" ) -// OpusPacketHeaderSize is the size of the opus packet header. -const OpusPacketHeaderSize = 12 +const ( + // OpusPacketHeaderSize is the size of the opus packet header. + OpusPacketHeaderSize = 12 + + // UDPTimeout is the timeout for UDP connections. + UDPTimeout = 30 * time.Second +) // ErrDecryptionFailed is returned when the packet decryption fails. var ErrDecryptionFailed = errors.New("decryption failed") @@ -86,6 +92,7 @@ type ( func NewUDPConn(opts ...UDPConnConfigOpt) UDPConn { config := DefaultUDPConnConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "voice_conn_udp_conn")) return &udpConnImpl{ config: config, @@ -148,7 +155,7 @@ func (u *udpConnImpl) Open(ctx context.Context, ip string, port int, ssrc uint32 u.connMu.Lock() defer u.connMu.Unlock() host := net.JoinHostPort(ip, strconv.Itoa(port)) - u.config.Logger.Debugf("Opening UDPConn connection to: %s\n", host) + u.config.Logger.Debug("Opening UDPConn connection", slog.String("host", host)) var err error u.conn, err = u.config.Dialer.DialContext(ctx, "udp", host) if err != nil { diff --git a/voice/udp_conn_config.go b/voice/udp_conn_config.go index 7e860b0ad..f1f0af98f 100644 --- a/voice/udp_conn_config.go +++ b/voice/udp_conn_config.go @@ -1,23 +1,21 @@ package voice import ( + "log/slog" "net" - "time" - - "github.com/disgoorg/log" ) func DefaultUDPConnConfig() UDPConnConfig { return UDPConnConfig{ - Logger: log.Default(), + Logger: slog.Default(), Dialer: &net.Dialer{ - Timeout: 30 * time.Second, + Timeout: UDPTimeout, }, } } type UDPConnConfig struct { - Logger log.Logger + Logger *slog.Logger Dialer *net.Dialer } @@ -29,7 +27,7 @@ func (c *UDPConnConfig) Apply(opts []UDPConnConfigOpt) { } } -func WithUDPConnLogger(logger log.Logger) UDPConnConfigOpt { +func WithUDPConnLogger(logger *slog.Logger) UDPConnConfigOpt { return func(config *UDPConnConfig) { config.Logger = logger } diff --git a/webhook/webhook_client_impl.go b/webhook/webhook_client_impl.go index 0adbacda2..2a85d1381 100644 --- a/webhook/webhook_client_impl.go +++ b/webhook/webhook_client_impl.go @@ -2,6 +2,7 @@ package webhook import ( "context" + "log/slog" "net/url" "strings" @@ -36,6 +37,7 @@ func NewWithURL(webhookURL string, opts ...ConfigOpt) (Client, error) { func New(id snowflake.ID, token string, opts ...ConfigOpt) Client { config := DefaultConfig() config.Apply(opts) + config.Logger = config.Logger.With(slog.String("name", "webhook")) return &clientImpl{ id: id, diff --git a/webhook/webhook_config.go b/webhook/webhook_config.go index 4ebe725ba..510c953a8 100644 --- a/webhook/webhook_config.go +++ b/webhook/webhook_config.go @@ -1,7 +1,7 @@ package webhook import ( - "github.com/disgoorg/log" + "log/slog" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/rest" @@ -10,14 +10,14 @@ import ( // DefaultConfig is the default configuration for the webhook client func DefaultConfig() *Config { return &Config{ - Logger: log.Default(), + Logger: slog.Default(), DefaultAllowedMentions: &discord.DefaultAllowedMentions, } } // Config is the configuration for the webhook client type Config struct { - Logger log.Logger + Logger *slog.Logger RestClient rest.Client RestClientConfigOpts []rest.ConfigOpt Webhooks rest.Webhooks @@ -33,7 +33,7 @@ func (c *Config) Apply(opts []ConfigOpt) { opt(c) } if c.RestClient == nil { - c.RestClient = rest.NewClient("", c.RestClientConfigOpts...) + c.RestClient = rest.NewClient("", append([]rest.ConfigOpt{rest.WithLogger(c.Logger)}, c.RestClientConfigOpts...)...) } if c.Webhooks == nil { c.Webhooks = rest.NewWebhooks(c.RestClient) @@ -41,7 +41,7 @@ func (c *Config) Apply(opts []ConfigOpt) { } // WithLogger sets the logger for the webhook client -func WithLogger(logger log.Logger) ConfigOpt { +func WithLogger(logger *slog.Logger) ConfigOpt { return func(config *Config) { config.Logger = logger }