├── .github ├── release.yml └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── components.go ├── discord.go ├── discord_test.go ├── docs ├── GettingStarted.md ├── img │ └── discordgo.svg └── index.md ├── endpoints.go ├── event.go ├── eventhandlers.go ├── events.go ├── examples ├── README.md ├── airhorn │ ├── README.md │ ├── airhorn.dca │ └── main.go ├── auto_moderation │ ├── README.md │ └── main.go ├── autocomplete │ ├── README.md │ └── main.go ├── avatar │ ├── README.md │ └── main.go ├── components │ ├── README.md │ └── main.go ├── context_menus │ ├── README.md │ └── main.go ├── dm_pingpong │ ├── README.md │ └── main.go ├── echo │ ├── README.md │ └── main.go ├── linked_roles │ ├── go.mod │ ├── go.sum │ └── main.go ├── modals │ ├── README.md │ └── main.go ├── pingpong │ ├── README.md │ └── main.go ├── scheduled_events │ ├── README.md │ └── main.go ├── slash_commands │ ├── README.md │ └── main.go ├── stage_instance │ ├── README.md │ └── main.go ├── threads │ ├── README.md │ └── main.go └── voice_receive │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.mod ├── go.sum ├── interactions.go ├── interactions_test.go ├── locales.go ├── logging.go ├── message.go ├── message_test.go ├── mkdocs.yml ├── oauth2.go ├── oauth2_test.go ├── ratelimit.go ├── ratelimit_test.go ├── restapi.go ├── restapi_test.go ├── state.go ├── structs.go ├── structs_test.go ├── tools └── cmd │ └── eventhandlers │ └── main.go ├── user.go ├── user_test.go ├── util.go ├── util_test.go ├── voice.go ├── webhook.go └── wsapi.go /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: "Breaking changes" 4 | labels: 5 | - breaking changes 6 | 7 | - title: "New features" 8 | labels: 9 | - feature 10 | 11 | - title: "Other changes" 12 | labels: 13 | - "*" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | 5 | name: CI 6 | 7 | jobs: 8 | format: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v3 13 | with: 14 | go-version: 1.18 15 | - name: Code 16 | uses: actions/checkout@v3 17 | - name: Check diff between gofmt and code 18 | run: diff <(gofmt -d .) <(echo -n) 19 | 20 | test: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | go-version: [1.13, 1.14, 1.15, 1.16, 1.17, 1.18] 25 | steps: 26 | - name: Install Go 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | - name: Code 31 | uses: actions/checkout@v3 32 | - run: go test -v -race ./... 33 | 34 | lint: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Install Go 38 | uses: actions/setup-go@v3 39 | with: 40 | go-version: 1.18 41 | - name: Code 42 | uses: actions/checkout@v3 43 | - name: Go vet 44 | run: go vet -x ./... 45 | 46 | - name: GolangCI-Lint 47 | uses: golangci/golangci-lint-action@v3 48 | if: github.event.name == 'pull_request' 49 | with: 50 | only-new-issues: true 51 | skip-pkg-cache: true 52 | skip-build-cache: true 53 | 54 | - name: GolangCI-Lint 55 | if: github.event.name != 'pull_request' # See https://github.com/golangci/golangci-lint-action/issues/362 56 | run: | 57 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.45.2 58 | 59 | $(go env GOPATH)/bin/golangci-lint run 60 | 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE-specific metadata 2 | .idea/ 3 | 4 | # Environment variables. Useful for examples. 5 | .env 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | # - staticcheck 5 | # - unused 6 | - golint 7 | 8 | linters-settings: 9 | staticcheck: 10 | go: "1.13" 11 | 12 | checks: ["all"] 13 | 14 | unused: 15 | go: "1.13" 16 | 17 | issues: 18 | include: 19 | - EXC0002 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13.x 4 | - 1.14.x 5 | - 1.15.x 6 | - 1.16.x 7 | - 1.17.x 8 | - 1.18.x 9 | env: 10 | - GO111MODULE=on 11 | install: 12 | - go get github.com/bwmarrin/discordgo 13 | - go get -v . 14 | - go get -v golang.org/x/lint/golint 15 | script: 16 | - diff <(gofmt -d .) <(echo -n) 17 | - go vet -x ./... 18 | - golint -set_exit_status ./... 19 | - go test -v -race ./... 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | To start off you can check out existing Pull Requests and Issues to get a gasp of what problems we’re currently solving and what features you can implement. 4 | 5 | ## Issues 6 | 7 | Our issues are mostly used for bugs, however we welcome refactoring and conceptual issues. 8 | 9 | Any other conversation would belong and would be moved into “Discussions”. 10 | 11 | ## Discussions 12 | 13 | We use discussions for ideas, polls, announcements and help questions. 14 | 15 | Don’t hesitate to ask, we always would try to help. 16 | 17 | ## Pull Requests 18 | 19 | If you want to help us by improving existing or adding new features, you create what’s called a Pull Request (aka PR). It allows us to review your code, suggest changes and merge it. 20 | 21 | Here are some tips on how to make a good first PR: 22 | 23 | - When creating a PR, please consider a distinctive name and description for it, so the maintainers can understand what your PR changes / adds / removes. 24 | - It’s always a good idea to link documentation when implementing a new feature / endpoint 25 | - If you’re resolving an issue, don’t forget to [link it](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) in the description. 26 | - Enable the checkbox to allow maintainers to edit your PR and make commits in the PR branch when necessary. 27 | - We may ask for changes, usually through suggestions or pull request comments. You can apply suggestions right in the UI. Any other change needs to be done manually. 28 | - Don’t forget to mark PR comments resolved when you’re done applying the changes. 29 | - Be patient and don’t close and reopen your PR when no one responds, sometimes it might be held for a while. There might be a lot of reasons: release preparation, the feature is not significant, maintainers are busy, etc. 30 | 31 | 32 | When your changes are still incomplete (i.e. in Work In Progress state), you can still create a PR, but consider making it a draft. 33 | To make a draft PR, you can change the type of PR by clicking to a triangle next to the “Create Pull Request” button. 34 | 35 | Once you’re done, you can mark it as “Ready for review”, and we’ll get right on it. 36 | 37 | 38 | # Code style 39 | 40 | To standardize and make things less messy we have a certain code style, that is persistent throughout the codebase. 41 | 42 | ## Naming 43 | 44 | ### REST methods 45 | 46 | When naming a REST method, while it might seem counterintuitive, we specify the entity before the action verb (for GET endpoints we don’t specify one however). Here’s an example: 47 | 48 | > Endpoint name: Get Channel Message 49 | > 50 | > Method name: `ChannelMessage` 51 | 52 | > Endpoint name: Edit Channel Message 53 | > 54 | > Method name: `ChannelMessageEdit` 55 | 56 | ### Parameter structures 57 | 58 | When making a complex REST endpoint, sometimes you might need to implement a `Param` structure. This structure contains parameters for certain endpoint/set of endpoints. 59 | 60 | - If an endpoint/set of endpoints have mostly same parameters, it’s a good idea to use a single `Param` structure for them. Here’s an example: 61 | 62 | > Endpoint: `GuildMemberEdit` 63 | > 64 | > `Param` structure: `GuildMemberParams` 65 | - If an endpoint/set of endpoints have differentiating parameters, `Param` structure can be named after the endpoint’s verb. Here’s an example: 66 | 67 | > Endpoint: `ChannelMessageSendComplex` 68 | > 69 | > `Param` structure: `MessageSend` 70 | 71 | > Endpoint: `ChannelMessageEditComplex` 72 | > 73 | > `Param` structure: `MessageEdit` 74 | 75 | ### Events 76 | 77 | When naming an event, we follow gateway’s internal naming (which often matches with the official event name in the docs). Here’s an example: 78 | 79 | > Event name: Interaction Create (`INTERACTION_CREATE`) 80 | > 81 | > Structure name: `InteractionCreate` 82 | 83 | ## Returns 84 | 85 | In our REST functions we usually favor named returns instead of regular anonymous returns. This helps readability. 86 | 87 | Additionally we try to avoid naked return statements for functions with a long body. Since it’s easier to loose track of the return result. 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Bruce Marriner 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of discordgo nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DiscordGo 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/bwmarrin/discordgo.svg)](https://pkg.go.dev/github.com/bwmarrin/discordgo) [![Go Report Card](https://goreportcard.com/badge/github.com/bwmarrin/discordgo)](https://goreportcard.com/report/github.com/bwmarrin/discordgo) [![CI](https://github.com/bwmarrin/discordgo/actions/workflows/ci.yml/badge.svg)](https://github.com/bwmarrin/discordgo/actions/workflows/ci.yml) [![Discord Gophers](https://img.shields.io/badge/Discord%20Gophers-%23discordgo-blue.svg)](https://discord.gg/golang) [![Discord API](https://img.shields.io/badge/Discord%20API-%23go_discordgo-blue.svg)](https://discord.com/invite/discord-api) 4 | 5 | DiscordGo logo 6 | 7 | DiscordGo is a [Go](https://golang.org/) package that provides low level 8 | bindings to the [Discord](https://discord.com/) chat client API. DiscordGo 9 | has nearly complete support for all of the Discord API endpoints, websocket 10 | interface, and voice interface. 11 | 12 | If you would like to help the DiscordGo package please use 13 | [this link](https://discord.com/oauth2/authorize?client_id=173113690092994561&scope=bot) 14 | to add the official DiscordGo test bot **dgo** to your server. This provides 15 | indispensable help to this project. 16 | 17 | * See [dgVoice](https://github.com/bwmarrin/dgvoice) package for an example of 18 | additional voice helper functions and features for DiscordGo. 19 | 20 | * See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone 21 | tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with 22 | Discord (and DiscordGo). 23 | 24 | **For help with this package or general Go discussion, please join the [Discord 25 | Gophers](https://discord.gg/golang) chat server.** 26 | 27 | ## Getting Started 28 | 29 | ### Installing 30 | 31 | This assumes you already have a working Go environment, if not please see 32 | [this page](https://golang.org/doc/install) first. 33 | 34 | `go get` *will always pull the latest tagged release from the master branch.* 35 | 36 | ```sh 37 | go get github.com/bwmarrin/discordgo 38 | ``` 39 | 40 | ### Usage 41 | 42 | Import the package into your project. 43 | 44 | ```go 45 | import "github.com/bwmarrin/discordgo" 46 | ``` 47 | 48 | Construct a new Discord client which can be used to access the variety of 49 | Discord API functions and to set callback functions for Discord events. 50 | 51 | ```go 52 | discord, err := discordgo.New("Bot " + "authentication token") 53 | ``` 54 | 55 | See Documentation and Examples below for more detailed information. 56 | 57 | 58 | ## Documentation 59 | 60 | **NOTICE**: This library and the Discord API are unfinished. 61 | Because of that there may be major changes to library in the future. 62 | 63 | The DiscordGo code is fairly well documented at this point and is currently 64 | the only documentation available. Go reference (below) presents that information in a nice format. 65 | 66 | - [![Go Reference](https://pkg.go.dev/badge/github.com/bwmarrin/discordgo.svg)](https://pkg.go.dev/github.com/bwmarrin/discordgo) 67 | - Hand crafted documentation coming eventually. 68 | 69 | 70 | ## Examples 71 | 72 | Below is a list of examples and other projects using DiscordGo. Please submit 73 | an issue if you would like your project added or removed from this list. 74 | 75 | - [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) - A collection of example programs written with DiscordGo 76 | - [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) - A curated list of high quality projects using DiscordGo 77 | 78 | ## Troubleshooting 79 | For help with common problems please reference the 80 | [Troubleshooting](https://github.com/bwmarrin/discordgo/wiki/Troubleshooting) 81 | section of the project wiki. 82 | 83 | 84 | ## Contributing 85 | Contributions are very welcomed, however please follow the below guidelines. 86 | 87 | - First open an issue describing the bug or enhancement so it can be 88 | discussed. 89 | - Try to match current naming conventions as closely as possible. 90 | - This package is intended to be a low level direct mapping of the Discord API, 91 | so please avoid adding enhancements outside of that scope without first 92 | discussing it. 93 | - Create a Pull Request with your changes against the master branch. 94 | 95 | 96 | ## List of Discord APIs 97 | 98 | See [this chart](https://abal.moe/Discord/Libraries.html) for a feature 99 | comparison and list of other Discord API libraries. 100 | 101 | ## Special Thanks 102 | 103 | [Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs. 104 | -------------------------------------------------------------------------------- /discord.go: -------------------------------------------------------------------------------- 1 | // Discordgo - Discord bindings for Go 2 | // Available at https://github.com/bwmarrin/discordgo 3 | 4 | // Copyright 2015-2016 Bruce Marriner . All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | // This file contains high level helper functions and easy entry points for the 9 | // entire discordgo package. These functions are being developed and are very 10 | // experimental at this point. They will most likely change so please use the 11 | // low level functions if that's a problem. 12 | 13 | // Package discordgo provides Discord binding for Go 14 | package discordgo 15 | 16 | import ( 17 | "net/http" 18 | "runtime" 19 | "time" 20 | 21 | "github.com/gorilla/websocket" 22 | ) 23 | 24 | // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) 25 | const VERSION = "0.29.0" 26 | 27 | // New creates a new Discord session with provided token. 28 | // If the token is for a bot, it must be prefixed with "Bot " 29 | // e.g. "Bot ..." 30 | // Or if it is an OAuth2 token, it must be prefixed with "Bearer " 31 | // e.g. "Bearer ..." 32 | func New(token string) (s *Session, err error) { 33 | 34 | // Create an empty Session interface. 35 | s = &Session{ 36 | State: NewState(), 37 | Ratelimiter: NewRatelimiter(), 38 | StateEnabled: true, 39 | Compress: true, 40 | ShouldReconnectOnError: true, 41 | ShouldReconnectVoiceOnSessionError: true, 42 | ShouldRetryOnRateLimit: true, 43 | ShardID: 0, 44 | ShardCount: 1, 45 | MaxRestRetries: 3, 46 | Client: &http.Client{Timeout: (20 * time.Second)}, 47 | Dialer: websocket.DefaultDialer, 48 | UserAgent: "DiscordBot (https://github.com/bwmarrin/discordgo, v" + VERSION + ")", 49 | sequence: new(int64), 50 | LastHeartbeatAck: time.Now().UTC(), 51 | } 52 | 53 | // Initialize the Identify Package with defaults 54 | // These can be modified prior to calling Open() 55 | s.Identify.Compress = true 56 | s.Identify.LargeThreshold = 250 57 | s.Identify.Properties.OS = runtime.GOOS 58 | s.Identify.Properties.Browser = "DiscordGo v" + VERSION 59 | s.Identify.Intents = IntentsAllWithoutPrivileged 60 | s.Identify.Token = token 61 | s.Token = token 62 | 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /discord_test.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | ////////////////////////////////////////////////////////////////////////////// 13 | ////////////////////////////////////////////////////// VARS NEEDED FOR TESTING 14 | var ( 15 | dg *Session // Stores a global discordgo user session 16 | dgBot *Session // Stores a global discordgo bot session 17 | 18 | envOAuth2Token = os.Getenv("DG_OAUTH2_TOKEN") // Token to use when authenticating using OAuth2 token 19 | envBotToken = os.Getenv("DGB_TOKEN") // Token to use when authenticating the bot account 20 | envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests 21 | envChannel = os.Getenv("DG_CHANNEL") // Channel ID to use for tests 22 | envVoiceChannel = os.Getenv("DG_VOICE_CHANNEL") // Channel ID to use for tests 23 | envAdmin = os.Getenv("DG_ADMIN") // User ID of admin user to use for tests 24 | ) 25 | 26 | func TestMain(m *testing.M) { 27 | fmt.Println("Init is being called.") 28 | if envBotToken != "" { 29 | if d, err := New(envBotToken); err == nil { 30 | dgBot = d 31 | } 32 | } 33 | 34 | if envOAuth2Token == "" { 35 | envOAuth2Token = os.Getenv("DGU_TOKEN") 36 | } 37 | 38 | if envOAuth2Token != "" { 39 | if d, err := New(envOAuth2Token); err == nil { 40 | dg = d 41 | } 42 | } 43 | 44 | os.Exit(m.Run()) 45 | } 46 | 47 | ////////////////////////////////////////////////////////////////////////////// 48 | /////////////////////////////////////////////////////////////// START OF TESTS 49 | 50 | // TestNewToken tests the New() function with a Token. 51 | func TestNewToken(t *testing.T) { 52 | 53 | if envOAuth2Token == "" { 54 | t.Skip("Skipping New(token), DGU_TOKEN not set") 55 | } 56 | 57 | d, err := New(envOAuth2Token) 58 | if err != nil { 59 | t.Fatalf("New(envToken) returned error: %+v", err) 60 | } 61 | 62 | if d == nil { 63 | t.Fatal("New(envToken), d is nil, should be Session{}") 64 | } 65 | 66 | if d.Token == "" { 67 | t.Fatal("New(envToken), d.Token is empty, should be a valid Token.") 68 | } 69 | } 70 | 71 | func TestOpenClose(t *testing.T) { 72 | if envOAuth2Token == "" { 73 | t.Skip("Skipping TestClose, DGU_TOKEN not set") 74 | } 75 | 76 | d, err := New(envOAuth2Token) 77 | if err != nil { 78 | t.Fatalf("TestClose, New(envToken) returned error: %+v", err) 79 | } 80 | 81 | if err = d.Open(); err != nil { 82 | t.Fatalf("TestClose, d.Open failed: %+v", err) 83 | } 84 | 85 | // We need a better way to know the session is ready for use, 86 | // this is totally gross. 87 | start := time.Now() 88 | for { 89 | d.RLock() 90 | if d.DataReady { 91 | d.RUnlock() 92 | break 93 | } 94 | d.RUnlock() 95 | 96 | if time.Since(start) > 10*time.Second { 97 | t.Fatal("DataReady never became true.yy") 98 | } 99 | runtime.Gosched() 100 | } 101 | 102 | // TODO find a better way 103 | // Add a small sleep here to make sure heartbeat and other events 104 | // have enough time to get fired. Need a way to actually check 105 | // those events. 106 | time.Sleep(2 * time.Second) 107 | 108 | // UpdateStatus - maybe we move this into wsapi_test.go but the websocket 109 | // created here is needed. This helps tests that the websocket was setup 110 | // and it is working. 111 | if err = d.UpdateGameStatus(0, time.Now().String()); err != nil { 112 | t.Errorf("UpdateStatus error: %+v", err) 113 | } 114 | 115 | if err = d.Close(); err != nil { 116 | t.Fatalf("TestClose, d.Close failed: %+v", err) 117 | } 118 | } 119 | 120 | func TestAddHandler(t *testing.T) { 121 | 122 | testHandlerCalled := int32(0) 123 | testHandler := func(s *Session, m *MessageCreate) { 124 | atomic.AddInt32(&testHandlerCalled, 1) 125 | } 126 | 127 | interfaceHandlerCalled := int32(0) 128 | interfaceHandler := func(s *Session, i interface{}) { 129 | atomic.AddInt32(&interfaceHandlerCalled, 1) 130 | } 131 | 132 | bogusHandlerCalled := int32(0) 133 | bogusHandler := func(s *Session, se *Session) { 134 | atomic.AddInt32(&bogusHandlerCalled, 1) 135 | } 136 | 137 | d := Session{} 138 | d.AddHandler(testHandler) 139 | d.AddHandler(testHandler) 140 | 141 | d.AddHandler(interfaceHandler) 142 | d.AddHandler(bogusHandler) 143 | 144 | d.handleEvent(messageCreateEventType, &MessageCreate{}) 145 | d.handleEvent(messageDeleteEventType, &MessageDelete{}) 146 | 147 | <-time.After(500 * time.Millisecond) 148 | 149 | // testHandler will be called twice because it was added twice. 150 | if atomic.LoadInt32(&testHandlerCalled) != 2 { 151 | t.Fatalf("testHandler was not called twice.") 152 | } 153 | 154 | // interfaceHandler will be called twice, once for each event. 155 | if atomic.LoadInt32(&interfaceHandlerCalled) != 2 { 156 | t.Fatalf("interfaceHandler was not called twice.") 157 | } 158 | 159 | if atomic.LoadInt32(&bogusHandlerCalled) != 0 { 160 | t.Fatalf("bogusHandler was called.") 161 | } 162 | } 163 | 164 | func TestRemoveHandler(t *testing.T) { 165 | 166 | testHandlerCalled := int32(0) 167 | testHandler := func(s *Session, m *MessageCreate) { 168 | atomic.AddInt32(&testHandlerCalled, 1) 169 | } 170 | 171 | d := Session{} 172 | r := d.AddHandler(testHandler) 173 | 174 | d.handleEvent(messageCreateEventType, &MessageCreate{}) 175 | 176 | r() 177 | 178 | d.handleEvent(messageCreateEventType, &MessageCreate{}) 179 | 180 | <-time.After(500 * time.Millisecond) 181 | 182 | // testHandler will be called once, as it was removed in between calls. 183 | if atomic.LoadInt32(&testHandlerCalled) != 1 { 184 | t.Fatalf("testHandler was not called once.") 185 | } 186 | } 187 | 188 | func TestScheduledEvents(t *testing.T) { 189 | if dgBot == nil { 190 | t.Skip("Skipping, dgBot not set.") 191 | } 192 | 193 | beginAt := time.Now().Add(1 * time.Hour) 194 | endAt := time.Now().Add(2 * time.Hour) 195 | event, err := dgBot.GuildScheduledEventCreate(envGuild, &GuildScheduledEventParams{ 196 | Name: "Test Event", 197 | PrivacyLevel: GuildScheduledEventPrivacyLevelGuildOnly, 198 | ScheduledStartTime: &beginAt, 199 | ScheduledEndTime: &endAt, 200 | Description: "Awesome Test Event created on livestream", 201 | EntityType: GuildScheduledEventEntityTypeExternal, 202 | EntityMetadata: &GuildScheduledEventEntityMetadata{ 203 | Location: "https://discord.com", 204 | }, 205 | }) 206 | defer dgBot.GuildScheduledEventDelete(envGuild, event.ID) 207 | 208 | if err != nil || event.Name != "Test Event" { 209 | t.Fatal(err) 210 | } 211 | 212 | events, err := dgBot.GuildScheduledEvents(envGuild, true) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | 217 | var foundEvent *GuildScheduledEvent 218 | for _, e := range events { 219 | if e.ID == event.ID { 220 | foundEvent = e 221 | break 222 | } 223 | } 224 | if foundEvent.Name != event.Name { 225 | t.Fatal("err on GuildScheduledEvents endpoint. Missing Scheduled Event") 226 | } 227 | 228 | getEvent, err := dgBot.GuildScheduledEvent(envGuild, event.ID, true) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | if getEvent.Name != event.Name { 233 | t.Fatal("err on GuildScheduledEvent endpoint. Mismatched Scheduled Event") 234 | } 235 | 236 | eventUpdated, err := dgBot.GuildScheduledEventEdit(envGuild, event.ID, &GuildScheduledEventParams{Name: "Test Event Updated"}) 237 | if err != nil { 238 | t.Fatal(err) 239 | } 240 | 241 | if eventUpdated.Name != "Test Event Updated" { 242 | t.Fatal("err on GuildScheduledEventUpdate endpoint. Scheduled Event Name mismatch") 243 | } 244 | 245 | // Usage of 1 and 1 is just the pseudo data with the purpose to run all branches in the function without crashes. 246 | // see https://github.com/bwmarrin/discordgo/pull/1032#discussion_r815438303 for more details. 247 | users, err := dgBot.GuildScheduledEventUsers(envGuild, event.ID, 1, true, "1", "1") 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | if len(users) != 0 { 252 | t.Fatal("err on GuildScheduledEventUsers. Mismatch of event maybe occurred") 253 | } 254 | 255 | err = dgBot.GuildScheduledEventDelete(envGuild, event.ID) 256 | if err != nil { 257 | t.Fatal(err) 258 | } 259 | } 260 | 261 | func TestComplexScheduledEvents(t *testing.T) { 262 | if dgBot == nil { 263 | t.Skip("Skipping, dgBot not set.") 264 | } 265 | 266 | beginAt := time.Now().Add(1 * time.Hour) 267 | endAt := time.Now().Add(2 * time.Hour) 268 | event, err := dgBot.GuildScheduledEventCreate(envGuild, &GuildScheduledEventParams{ 269 | Name: "Test Voice Event", 270 | PrivacyLevel: GuildScheduledEventPrivacyLevelGuildOnly, 271 | ScheduledStartTime: &beginAt, 272 | ScheduledEndTime: &endAt, 273 | Description: "Test event on voice channel", 274 | EntityType: GuildScheduledEventEntityTypeVoice, 275 | ChannelID: envVoiceChannel, 276 | }) 277 | if err != nil || event.Name != "Test Voice Event" { 278 | t.Fatal(err) 279 | } 280 | defer dgBot.GuildScheduledEventDelete(envGuild, event.ID) 281 | 282 | _, err = dgBot.GuildScheduledEventEdit(envGuild, event.ID, &GuildScheduledEventParams{ 283 | EntityType: GuildScheduledEventEntityTypeExternal, 284 | EntityMetadata: &GuildScheduledEventEntityMetadata{ 285 | Location: "https://discord.com", 286 | }, 287 | }) 288 | 289 | if err != nil { 290 | t.Fatal("err on GuildScheduledEventEdit. Change of entity type to external failed") 291 | } 292 | 293 | _, err = dgBot.GuildScheduledEventEdit(envGuild, event.ID, &GuildScheduledEventParams{ 294 | ChannelID: envVoiceChannel, 295 | EntityType: GuildScheduledEventEntityTypeVoice, 296 | EntityMetadata: nil, 297 | }) 298 | 299 | if err != nil { 300 | t.Fatal("err on GuildScheduledEventEdit. Change of entity type to voice failed") 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /docs/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This page is dedicated to helping you get started on your way to making the 4 | next great Discord bot or client with DiscordGo. Once you've done that please 5 | don't forget to submit it to the 6 | [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) list :). 7 | 8 | 9 | **First, lets cover a few topics so you can make the best choices on how to 10 | move forward from here.** 11 | 12 | #### Bot Application 13 | A bot application is a special program that interacts with the Discord servers 14 | to perform some form of automation or provide some type of service. Examples 15 | are things like number trivia games, music streaming, channel moderation, 16 | sending reminders, playing loud airhorn sounds, comic generators, YouTube 17 | integration, Twitch integration... You're *almost* only limited by your imagination. 18 | 19 | Bot applications require the use of a special Bot account. These accounts are 20 | tied to your personal user account. Bot accounts cannot login with the normal 21 | user clients and they cannot join servers the same way a user does. They do not 22 | have access to some user client specific features however they gain access to 23 | many Bot specific features. 24 | 25 | To create a new bot account first create yourself a normal user account on 26 | Discord then visit the [My Applications](https://discord.com/developers/applications/me) 27 | page and click on the **New Application** box. Follow the prompts from there 28 | to finish creating your account. 29 | 30 | 31 | **More information about Bot vs Client accounts can be found [here](https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts).** 32 | 33 | # Requirements 34 | 35 | DiscordGo requires Go version 1.4 or higher. It has been tested to compile and 36 | run successfully on Debian Linux 8, FreeBSD 10, and Windows 7. It is expected 37 | that it should work anywhere Go 1.4 or higher works. If you run into problems 38 | please let us know :). 39 | 40 | You must already have a working Go environment setup to use DiscordGo. If you 41 | are new to Go and have not yet installed and tested it on your computer then 42 | please visit [this page](https://golang.org/doc/install) first then I highly 43 | recommend you walk though [A Tour of Go](https://tour.golang.org/welcome/1) to 44 | help get your familiar with the Go language. Also checkout the relevant Go plugin 45 | for your editor — they are hugely helpful when developing Go code. 46 | 47 | * Vim — [vim-go](https://github.com/fatih/vim-go) 48 | * Sublime — [GoSublime](https://github.com/DisposaBoy/GoSublime) 49 | * Atom — [go-plus](https://atom.io/packages/go-plus) 50 | * Visual Studio — [vscode-go](https://github.com/Microsoft/vscode-go) 51 | 52 | 53 | # Install DiscordGo 54 | 55 | Like any other Go package the fist step is to `go get` the package. This will 56 | always pull the latest tagged release from the master branch. Then run 57 | `go install` to compile and install the libraries on your system. 58 | 59 | #### Linux/BSD 60 | 61 | Run go get to download the package to your GOPATH/src folder. 62 | 63 | ```sh 64 | go get github.com/bwmarrin/discordgo 65 | ``` 66 | 67 | Finally, compile and install the package into the GOPATH/pkg folder. This isn't 68 | absolutely required but doing this will allow the Go plugin for your editor to 69 | provide autocomplete for all DiscordGo functions. 70 | 71 | ```sh 72 | cd $GOPATH/src/github.com/bwmarrin/discordgo 73 | go install 74 | ``` 75 | 76 | #### Windows 77 | Placeholder. 78 | 79 | 80 | # Next... 81 | More coming soon. 82 | -------------------------------------------------------------------------------- /docs/img/discordgo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## DiscordGo 2 |
3 | 4 | 5 | [Go](https://golang.org/) (Golang) interface for the [Discord](https://discord.com/) 6 | chat service. Provides both low-level direct bindings to the 7 | Discord API and helper functions that allow you to make custom clients and chat 8 | bot applications easily. 9 | 10 | [Discord](https://discord.com/) is an all-in-one voice and text chat for 11 | gamers that's free, secure, and works on both your desktop and phone. 12 | 13 | ### Why DiscordGo? 14 | * High Performance 15 | * Minimal Memory & CPU Load 16 | * Low-level bindings to Discord REST API Endpoints 17 | * Support for the data websocket interface 18 | * Multi-Server voice connections (send and receive) 19 | * State tracking and caching 20 | 21 | ### Learn More 22 | * Check out the [Getting Started](GettingStarted.md) section 23 | * Read the reference docs on [Godoc](https://godoc.org/github.com/bwmarrin/discordgo) or [GoWalker](https://gowalker.org/github.com/bwmarrin/discordgo) 24 | * Try the [examples](https://github.com/bwmarrin/discordgo/tree/master/examples) 25 | * Explore [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) 26 | 27 | ### Join Us! 28 | Both of the below links take you to chat channels where you can get more 29 | information and support for DiscordGo. There's also a chance to make some 30 | friends :). 31 | 32 | * Join the [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) chat server dedicated to Go programming. 33 | * Join the [Discord API](https://discord.com/invite/discord-API) chat server dedicated to the Discord API. 34 | -------------------------------------------------------------------------------- /endpoints.go: -------------------------------------------------------------------------------- 1 | // Discordgo - Discord bindings for Go 2 | // Available at https://github.com/bwmarrin/discordgo 3 | 4 | // Copyright 2015-2016 Bruce Marriner . All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | // This file contains variables for all known Discord end points. All functions 9 | // throughout the Discordgo package use these variables for all connections 10 | // to Discord. These are all exported and you may modify them if needed. 11 | 12 | package discordgo 13 | 14 | import "strconv" 15 | 16 | // APIVersion is the Discord API version used for the REST and Websocket API. 17 | var APIVersion = "9" 18 | 19 | // Known Discord API Endpoints. 20 | var ( 21 | EndpointStatus = "https://status.discord.com/api/v2/" 22 | EndpointSm = EndpointStatus + "scheduled-maintenances/" 23 | EndpointSmActive = EndpointSm + "active.json" 24 | EndpointSmUpcoming = EndpointSm + "upcoming.json" 25 | 26 | EndpointDiscord = "https://discord.com/" 27 | EndpointAPI = EndpointDiscord + "api/v" + APIVersion + "/" 28 | EndpointGuilds = EndpointAPI + "guilds/" 29 | EndpointChannels = EndpointAPI + "channels/" 30 | EndpointUsers = EndpointAPI + "users/" 31 | EndpointGateway = EndpointAPI + "gateway" 32 | EndpointGatewayBot = EndpointGateway + "/bot" 33 | EndpointWebhooks = EndpointAPI + "webhooks/" 34 | EndpointStickers = EndpointAPI + "stickers/" 35 | EndpointStageInstances = EndpointAPI + "stage-instances" 36 | EndpointSKUs = EndpointAPI + "skus" 37 | 38 | EndpointCDN = "https://cdn.discordapp.com/" 39 | EndpointCDNAttachments = EndpointCDN + "attachments/" 40 | EndpointCDNAvatars = EndpointCDN + "avatars/" 41 | EndpointCDNIcons = EndpointCDN + "icons/" 42 | EndpointCDNSplashes = EndpointCDN + "splashes/" 43 | EndpointCDNChannelIcons = EndpointCDN + "channel-icons/" 44 | EndpointCDNBanners = EndpointCDN + "banners/" 45 | EndpointCDNGuilds = EndpointCDN + "guilds/" 46 | EndpointCDNRoleIcons = EndpointCDN + "role-icons/" 47 | 48 | EndpointVoice = EndpointAPI + "/voice/" 49 | EndpointVoiceRegions = EndpointVoice + "regions" 50 | 51 | EndpointUser = func(uID string) string { return EndpointUsers + uID } 52 | EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } 53 | EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" } 54 | EndpointDefaultUserAvatar = func(idx int) string { 55 | return EndpointCDN + "embed/avatars/" + strconv.Itoa(idx) + ".png" 56 | } 57 | EndpointUserBanner = func(uID, cID string) string { 58 | return EndpointCDNBanners + uID + "/" + cID + ".png" 59 | } 60 | EndpointUserBannerAnimated = func(uID, cID string) string { 61 | return EndpointCDNBanners + uID + "/" + cID + ".gif" 62 | } 63 | 64 | EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } 65 | EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } 66 | EndpointUserGuildMember = func(uID, gID string) string { return EndpointUserGuild(uID, gID) + "/member" } 67 | EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } 68 | EndpointUserApplicationRoleConnection = func(aID string) string { return EndpointUsers + "@me/applications/" + aID + "/role-connection" } 69 | EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } 70 | 71 | EndpointGuild = func(gID string) string { return EndpointGuilds + gID } 72 | EndpointGuildAutoModeration = func(gID string) string { return EndpointGuild(gID) + "/auto-moderation" } 73 | EndpointGuildAutoModerationRules = func(gID string) string { return EndpointGuildAutoModeration(gID) + "/rules" } 74 | EndpointGuildAutoModerationRule = func(gID, rID string) string { return EndpointGuildAutoModerationRules(gID) + "/" + rID } 75 | EndpointGuildThreads = func(gID string) string { return EndpointGuild(gID) + "/threads" } 76 | EndpointGuildActiveThreads = func(gID string) string { return EndpointGuildThreads(gID) + "/active" } 77 | EndpointGuildPreview = func(gID string) string { return EndpointGuilds + gID + "/preview" } 78 | EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } 79 | EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" } 80 | EndpointGuildMembersSearch = func(gID string) string { return EndpointGuildMembers(gID) + "/search" } 81 | EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID } 82 | EndpointGuildMemberRole = func(gID, uID, rID string) string { return EndpointGuilds + gID + "/members/" + uID + "/roles/" + rID } 83 | EndpointGuildBans = func(gID string) string { return EndpointGuilds + gID + "/bans" } 84 | EndpointGuildBan = func(gID, uID string) string { return EndpointGuilds + gID + "/bans/" + uID } 85 | EndpointGuildIntegrations = func(gID string) string { return EndpointGuilds + gID + "/integrations" } 86 | EndpointGuildIntegration = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID } 87 | EndpointGuildRoles = func(gID string) string { return EndpointGuilds + gID + "/roles" } 88 | EndpointGuildRole = func(gID, rID string) string { return EndpointGuilds + gID + "/roles/" + rID } 89 | EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" } 90 | EndpointGuildWidget = func(gID string) string { return EndpointGuilds + gID + "/widget" } 91 | EndpointGuildEmbed = EndpointGuildWidget 92 | EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } 93 | EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" } 94 | EndpointGuildIconAnimated = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".gif" } 95 | EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } 96 | EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } 97 | EndpointGuildAuditLogs = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" } 98 | EndpointGuildEmojis = func(gID string) string { return EndpointGuilds + gID + "/emojis" } 99 | EndpointGuildEmoji = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID } 100 | EndpointGuildBanner = func(gID, hash string) string { return EndpointCDNBanners + gID + "/" + hash + ".png" } 101 | EndpointGuildBannerAnimated = func(gID, hash string) string { return EndpointCDNBanners + gID + "/" + hash + ".gif" } 102 | EndpointGuildStickers = func(gID string) string { return EndpointGuilds + gID + "/stickers" } 103 | EndpointGuildSticker = func(gID, sID string) string { return EndpointGuilds + gID + "/stickers/" + sID } 104 | EndpointStageInstance = func(cID string) string { return EndpointStageInstances + "/" + cID } 105 | EndpointGuildScheduledEvents = func(gID string) string { return EndpointGuilds + gID + "/scheduled-events" } 106 | EndpointGuildScheduledEvent = func(gID, eID string) string { return EndpointGuilds + gID + "/scheduled-events/" + eID } 107 | EndpointGuildScheduledEventUsers = func(gID, eID string) string { return EndpointGuildScheduledEvent(gID, eID) + "/users" } 108 | EndpointGuildOnboarding = func(gID string) string { return EndpointGuilds + gID + "/onboarding" } 109 | EndpointGuildTemplate = func(tID string) string { return EndpointGuilds + "templates/" + tID } 110 | EndpointGuildTemplates = func(gID string) string { return EndpointGuilds + gID + "/templates" } 111 | EndpointGuildTemplateSync = func(gID, tID string) string { return EndpointGuilds + gID + "/templates/" + tID } 112 | EndpointGuildMemberAvatar = func(gId, uID, aID string) string { 113 | return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".png" 114 | } 115 | EndpointGuildMemberAvatarAnimated = func(gId, uID, aID string) string { 116 | return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".gif" 117 | } 118 | EndpointGuildMemberBanner = func(gId, uID, hash string) string { 119 | return EndpointCDNGuilds + gId + "/users/" + uID + "/banners/" + hash + ".png" 120 | } 121 | EndpointGuildMemberBannerAnimated = func(gId, uID, hash string) string { 122 | return EndpointCDNGuilds + gId + "/users/" + uID + "/banners/" + hash + ".gif" 123 | } 124 | 125 | EndpointRoleIcon = func(rID, hash string) string { 126 | return EndpointCDNRoleIcons + rID + "/" + hash + ".png" 127 | } 128 | 129 | EndpointChannel = func(cID string) string { return EndpointChannels + cID } 130 | EndpointChannelThreads = func(cID string) string { return EndpointChannel(cID) + "/threads" } 131 | EndpointChannelActiveThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/active" } 132 | EndpointChannelPublicArchivedThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/public" } 133 | EndpointChannelPrivateArchivedThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/private" } 134 | EndpointChannelJoinedPrivateArchivedThreads = func(cID string) string { return EndpointChannel(cID) + "/users/@me/threads/archived/private" } 135 | EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } 136 | EndpointChannelPermission = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID } 137 | EndpointChannelInvites = func(cID string) string { return EndpointChannels + cID + "/invites" } 138 | EndpointChannelTyping = func(cID string) string { return EndpointChannels + cID + "/typing" } 139 | EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" } 140 | EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID } 141 | EndpointChannelMessageThread = func(cID, mID string) string { return EndpointChannelMessage(cID, mID) + "/threads" } 142 | EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" } 143 | EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } 144 | EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } 145 | EndpointChannelMessageCrosspost = func(cID, mID string) string { return EndpointChannel(cID) + "/messages/" + mID + "/crosspost" } 146 | EndpointChannelFollow = func(cID string) string { return EndpointChannel(cID) + "/followers" } 147 | EndpointThreadMembers = func(tID string) string { return EndpointChannel(tID) + "/thread-members" } 148 | EndpointThreadMember = func(tID, mID string) string { return EndpointThreadMembers(tID) + "/" + mID } 149 | 150 | EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" } 151 | 152 | EndpointSticker = func(sID string) string { return EndpointStickers + sID } 153 | EndpointNitroStickersPacks = EndpointAPI + "/sticker-packs" 154 | 155 | EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" } 156 | EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } 157 | EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } 158 | EndpointWebhookMessage = func(wID, token, messageID string) string { 159 | return EndpointWebhookToken(wID, token) + "/messages/" + messageID 160 | } 161 | 162 | EndpointMessageReactionsAll = func(cID, mID string) string { 163 | return EndpointChannelMessage(cID, mID) + "/reactions" 164 | } 165 | EndpointMessageReactions = func(cID, mID, eID string) string { 166 | return EndpointChannelMessage(cID, mID) + "/reactions/" + eID 167 | } 168 | EndpointMessageReaction = func(cID, mID, eID, uID string) string { 169 | return EndpointMessageReactions(cID, mID, eID) + "/" + uID 170 | } 171 | 172 | EndpointPoll = func(cID, mID string) string { 173 | return EndpointChannel(cID) + "/polls/" + mID 174 | } 175 | EndpointPollAnswerVoters = func(cID, mID string, aID int) string { 176 | return EndpointPoll(cID, mID) + "/answers/" + strconv.Itoa(aID) 177 | } 178 | EndpointPollExpire = func(cID, mID string) string { 179 | return EndpointPoll(cID, mID) + "/expire" 180 | } 181 | 182 | EndpointApplicationSKUs = func(aID string) string { 183 | return EndpointApplication(aID) + "/skus" 184 | } 185 | 186 | EndpointEntitlements = func(aID string) string { 187 | return EndpointApplication(aID) + "/entitlements" 188 | } 189 | EndpointEntitlement = func(aID, eID string) string { 190 | return EndpointEntitlements(aID) + "/" + eID 191 | } 192 | EndpointEntitlementConsume = func(aID, eID string) string { 193 | return EndpointEntitlement(aID, eID) + "/consume" 194 | } 195 | 196 | EndpointSubscriptions = func(skuID string) string { 197 | return EndpointSKUs + "/" + skuID + "/subscriptions" 198 | } 199 | EndpointSubscription = func(skuID, subID string) string { 200 | return EndpointSubscriptions(skuID) + "/" + subID 201 | } 202 | 203 | EndpointApplicationGlobalCommands = func(aID string) string { 204 | return EndpointApplication(aID) + "/commands" 205 | } 206 | EndpointApplicationGlobalCommand = func(aID, cID string) string { 207 | return EndpointApplicationGlobalCommands(aID) + "/" + cID 208 | } 209 | 210 | EndpointApplicationGuildCommands = func(aID, gID string) string { 211 | return EndpointApplication(aID) + "/guilds/" + gID + "/commands" 212 | } 213 | EndpointApplicationGuildCommand = func(aID, gID, cID string) string { 214 | return EndpointApplicationGuildCommands(aID, gID) + "/" + cID 215 | } 216 | EndpointApplicationCommandPermissions = func(aID, gID, cID string) string { 217 | return EndpointApplicationGuildCommand(aID, gID, cID) + "/permissions" 218 | } 219 | EndpointApplicationCommandsGuildPermissions = func(aID, gID string) string { 220 | return EndpointApplicationGuildCommands(aID, gID) + "/permissions" 221 | } 222 | EndpointInteraction = func(aID, iToken string) string { 223 | return EndpointAPI + "interactions/" + aID + "/" + iToken 224 | } 225 | EndpointInteractionResponse = func(iID, iToken string) string { 226 | return EndpointInteraction(iID, iToken) + "/callback" 227 | } 228 | EndpointInteractionResponseActions = func(aID, iToken string) string { 229 | return EndpointWebhookMessage(aID, iToken, "@original") 230 | } 231 | EndpointFollowupMessage = func(aID, iToken string) string { 232 | return EndpointWebhookToken(aID, iToken) 233 | } 234 | EndpointFollowupMessageActions = func(aID, iToken, mID string) string { 235 | return EndpointWebhookMessage(aID, iToken, mID) 236 | } 237 | 238 | EndpointGuildCreate = EndpointAPI + "guilds" 239 | 240 | EndpointInvite = func(iID string) string { return EndpointAPI + "invites/" + iID } 241 | 242 | EndpointEmoji = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".png" } 243 | EndpointEmojiAnimated = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".gif" } 244 | 245 | EndpointApplications = EndpointAPI + "applications" 246 | EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID } 247 | EndpointApplicationRoleConnectionMetadata = func(aID string) string { return EndpointApplication(aID) + "/role-connections/metadata" } 248 | 249 | EndpointApplicationEmojis = func(aID string) string { return EndpointApplication(aID) + "/emojis" } 250 | EndpointApplicationEmoji = func(aID, eID string) string { return EndpointApplication(aID) + "/emojis/" + eID } 251 | 252 | EndpointOAuth2 = EndpointAPI + "oauth2/" 253 | EndpointOAuth2Applications = EndpointOAuth2 + "applications" 254 | EndpointOAuth2Application = func(aID string) string { return EndpointOAuth2Applications + "/" + aID } 255 | EndpointOAuth2ApplicationsBot = func(aID string) string { return EndpointOAuth2Applications + "/" + aID + "/bot" } 256 | EndpointOAuth2ApplicationAssets = func(aID string) string { return EndpointOAuth2Applications + "/" + aID + "/assets" } 257 | 258 | // TODO: Deprecated, remove in the next release 259 | EndpointOauth2 = EndpointOAuth2 260 | EndpointOauth2Applications = EndpointOAuth2Applications 261 | EndpointOauth2Application = EndpointOAuth2Application 262 | EndpointOauth2ApplicationsBot = EndpointOAuth2ApplicationsBot 263 | EndpointOauth2ApplicationAssets = EndpointOAuth2ApplicationAssets 264 | ) 265 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | // EventHandler is an interface for Discord events. 4 | type EventHandler interface { 5 | // Type returns the type of event this handler belongs to. 6 | Type() string 7 | 8 | // Handle is called whenever an event of Type() happens. 9 | // It is the receivers responsibility to type assert that the interface 10 | // is the expected struct. 11 | Handle(*Session, interface{}) 12 | } 13 | 14 | // EventInterfaceProvider is an interface for providing empty interfaces for 15 | // Discord events. 16 | type EventInterfaceProvider interface { 17 | // Type is the type of event this handler belongs to. 18 | Type() string 19 | 20 | // New returns a new instance of the struct this event handler handles. 21 | // This is called once per event. 22 | // The struct is provided to all handlers of the same Type(). 23 | New() interface{} 24 | } 25 | 26 | // interfaceEventType is the event handler type for interface{} events. 27 | const interfaceEventType = "__INTERFACE__" 28 | 29 | // interfaceEventHandler is an event handler for interface{} events. 30 | type interfaceEventHandler func(*Session, interface{}) 31 | 32 | // Type returns the event type for interface{} events. 33 | func (eh interfaceEventHandler) Type() string { 34 | return interfaceEventType 35 | } 36 | 37 | // Handle is the handler for an interface{} event. 38 | func (eh interfaceEventHandler) Handle(s *Session, i interface{}) { 39 | eh(s, i) 40 | } 41 | 42 | var registeredInterfaceProviders = map[string]EventInterfaceProvider{} 43 | 44 | // registerInterfaceProvider registers a provider so that DiscordGo can 45 | // access it's New() method. 46 | func registerInterfaceProvider(eh EventInterfaceProvider) { 47 | if _, ok := registeredInterfaceProviders[eh.Type()]; ok { 48 | return 49 | // XXX: 50 | // if we should error here, we need to do something with it. 51 | // fmt.Errorf("event %s already registered", eh.Type()) 52 | } 53 | registeredInterfaceProviders[eh.Type()] = eh 54 | return 55 | } 56 | 57 | // eventHandlerInstance is a wrapper around an event handler, as functions 58 | // cannot be compared directly. 59 | type eventHandlerInstance struct { 60 | eventHandler EventHandler 61 | } 62 | 63 | // addEventHandler adds an event handler that will be fired anytime 64 | // the Discord WSAPI matching eventHandler.Type() fires. 65 | func (s *Session) addEventHandler(eventHandler EventHandler) func() { 66 | s.handlersMu.Lock() 67 | defer s.handlersMu.Unlock() 68 | 69 | if s.handlers == nil { 70 | s.handlers = map[string][]*eventHandlerInstance{} 71 | } 72 | 73 | ehi := &eventHandlerInstance{eventHandler} 74 | s.handlers[eventHandler.Type()] = append(s.handlers[eventHandler.Type()], ehi) 75 | 76 | return func() { 77 | s.removeEventHandlerInstance(eventHandler.Type(), ehi) 78 | } 79 | } 80 | 81 | // addEventHandler adds an event handler that will be fired the next time 82 | // the Discord WSAPI matching eventHandler.Type() fires. 83 | func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() { 84 | s.handlersMu.Lock() 85 | defer s.handlersMu.Unlock() 86 | 87 | if s.onceHandlers == nil { 88 | s.onceHandlers = map[string][]*eventHandlerInstance{} 89 | } 90 | 91 | ehi := &eventHandlerInstance{eventHandler} 92 | s.onceHandlers[eventHandler.Type()] = append(s.onceHandlers[eventHandler.Type()], ehi) 93 | 94 | return func() { 95 | s.removeEventHandlerInstance(eventHandler.Type(), ehi) 96 | } 97 | } 98 | 99 | // AddHandler allows you to add an event handler that will be fired anytime 100 | // the Discord WSAPI event that matches the function fires. 101 | // The first parameter is a *Session, and the second parameter is a pointer 102 | // to a struct corresponding to the event for which you want to listen. 103 | // 104 | // eg: 105 | // Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { 106 | // }) 107 | // 108 | // or: 109 | // Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { 110 | // }) 111 | // 112 | // List of events can be found at this page, with corresponding names in the 113 | // library for each event: https://discord.com/developers/docs/topics/gateway#event-names 114 | // There are also synthetic events fired by the library internally which are 115 | // available for handling, like Connect, Disconnect, and RateLimit. 116 | // events.go contains all of the Discord WSAPI and synthetic events that can be handled. 117 | // 118 | // The return value of this method is a function, that when called will remove the 119 | // event handler. 120 | func (s *Session) AddHandler(handler interface{}) func() { 121 | eh := handlerForInterface(handler) 122 | 123 | if eh == nil { 124 | s.log(LogError, "Invalid handler type, handler will never be called") 125 | return func() {} 126 | } 127 | 128 | return s.addEventHandler(eh) 129 | } 130 | 131 | // AddHandlerOnce allows you to add an event handler that will be fired the next time 132 | // the Discord WSAPI event that matches the function fires. 133 | // See AddHandler for more details. 134 | func (s *Session) AddHandlerOnce(handler interface{}) func() { 135 | eh := handlerForInterface(handler) 136 | 137 | if eh == nil { 138 | s.log(LogError, "Invalid handler type, handler will never be called") 139 | return func() {} 140 | } 141 | 142 | return s.addEventHandlerOnce(eh) 143 | } 144 | 145 | // removeEventHandler instance removes an event handler instance. 146 | func (s *Session) removeEventHandlerInstance(t string, ehi *eventHandlerInstance) { 147 | s.handlersMu.Lock() 148 | defer s.handlersMu.Unlock() 149 | 150 | handlers := s.handlers[t] 151 | for i := range handlers { 152 | if handlers[i] == ehi { 153 | s.handlers[t] = append(handlers[:i], handlers[i+1:]...) 154 | } 155 | } 156 | 157 | onceHandlers := s.onceHandlers[t] 158 | for i := range onceHandlers { 159 | if onceHandlers[i] == ehi { 160 | s.onceHandlers[t] = append(onceHandlers[:i], onceHandlers[i+1:]...) 161 | } 162 | } 163 | } 164 | 165 | // Handles calling permanent and once handlers for an event type. 166 | func (s *Session) handle(t string, i interface{}) { 167 | for _, eh := range s.handlers[t] { 168 | if s.SyncEvents { 169 | eh.eventHandler.Handle(s, i) 170 | } else { 171 | go eh.eventHandler.Handle(s, i) 172 | } 173 | } 174 | 175 | if len(s.onceHandlers[t]) > 0 { 176 | for _, eh := range s.onceHandlers[t] { 177 | if s.SyncEvents { 178 | eh.eventHandler.Handle(s, i) 179 | } else { 180 | go eh.eventHandler.Handle(s, i) 181 | } 182 | } 183 | s.onceHandlers[t] = nil 184 | } 185 | } 186 | 187 | // Handles an event type by calling internal methods, firing handlers and firing the 188 | // interface{} event. 189 | func (s *Session) handleEvent(t string, i interface{}) { 190 | s.handlersMu.RLock() 191 | defer s.handlersMu.RUnlock() 192 | 193 | // All events are dispatched internally first. 194 | s.onInterface(i) 195 | 196 | // Then they are dispatched to anyone handling interface{} events. 197 | s.handle(interfaceEventType, i) 198 | 199 | // Finally they are dispatched to any typed handlers. 200 | s.handle(t, i) 201 | } 202 | 203 | // setGuildIds will set the GuildID on all the members of a guild. 204 | // This is done as event data does not have it set. 205 | func setGuildIds(g *Guild) { 206 | for _, c := range g.Channels { 207 | c.GuildID = g.ID 208 | } 209 | 210 | for _, m := range g.Members { 211 | m.GuildID = g.ID 212 | } 213 | 214 | for _, vs := range g.VoiceStates { 215 | vs.GuildID = g.ID 216 | } 217 | } 218 | 219 | // onInterface handles all internal events and routes them to the appropriate internal handler. 220 | func (s *Session) onInterface(i interface{}) { 221 | switch t := i.(type) { 222 | case *Ready: 223 | for _, g := range t.Guilds { 224 | setGuildIds(g) 225 | } 226 | s.onReady(t) 227 | case *GuildCreate: 228 | setGuildIds(t.Guild) 229 | case *GuildUpdate: 230 | setGuildIds(t.Guild) 231 | case *VoiceServerUpdate: 232 | go s.onVoiceServerUpdate(t) 233 | case *VoiceStateUpdate: 234 | go s.onVoiceStateUpdate(t) 235 | } 236 | err := s.State.OnInterface(s, i) 237 | if err != nil { 238 | s.log(LogDebug, "error dispatching internal event, %s", err) 239 | } 240 | } 241 | 242 | // onReady handles the ready event. 243 | func (s *Session) onReady(r *Ready) { 244 | 245 | // Store the SessionID within the Session struct. 246 | s.sessionID = r.SessionID 247 | } 248 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // This file contains all the possible structs that can be 8 | // handled by AddHandler/EventHandler. 9 | // DO NOT ADD ANYTHING BUT EVENT HANDLER STRUCTS TO THIS FILE. 10 | //go:generate go run tools/cmd/eventhandlers/main.go 11 | 12 | // Connect is the data for a Connect event. 13 | // This is a synthetic event and is not dispatched by Discord. 14 | type Connect struct{} 15 | 16 | // Disconnect is the data for a Disconnect event. 17 | // This is a synthetic event and is not dispatched by Discord. 18 | type Disconnect struct{} 19 | 20 | // RateLimit is the data for a RateLimit event. 21 | // This is a synthetic event and is not dispatched by Discord. 22 | type RateLimit struct { 23 | *TooManyRequests 24 | URL string 25 | } 26 | 27 | // Event provides a basic initial struct for all websocket events. 28 | type Event struct { 29 | Operation int `json:"op"` 30 | Sequence int64 `json:"s"` 31 | Type string `json:"t"` 32 | RawData json.RawMessage `json:"d"` 33 | // Struct contains one of the other types in this file. 34 | Struct interface{} `json:"-"` 35 | } 36 | 37 | // A Ready stores all data for the websocket READY event. 38 | type Ready struct { 39 | Version int `json:"v"` 40 | SessionID string `json:"session_id"` 41 | User *User `json:"user"` 42 | Shard *[2]int `json:"shard"` 43 | Application *Application `json:"application"` 44 | Guilds []*Guild `json:"guilds"` 45 | PrivateChannels []*Channel `json:"private_channels"` 46 | } 47 | 48 | // ChannelCreate is the data for a ChannelCreate event. 49 | type ChannelCreate struct { 50 | *Channel 51 | } 52 | 53 | // ChannelUpdate is the data for a ChannelUpdate event. 54 | type ChannelUpdate struct { 55 | *Channel 56 | BeforeUpdate *Channel `json:"-"` 57 | } 58 | 59 | // ChannelDelete is the data for a ChannelDelete event. 60 | type ChannelDelete struct { 61 | *Channel 62 | } 63 | 64 | // ChannelPinsUpdate stores data for a ChannelPinsUpdate event. 65 | type ChannelPinsUpdate struct { 66 | LastPinTimestamp string `json:"last_pin_timestamp"` 67 | ChannelID string `json:"channel_id"` 68 | GuildID string `json:"guild_id,omitempty"` 69 | } 70 | 71 | // ThreadCreate is the data for a ThreadCreate event. 72 | type ThreadCreate struct { 73 | *Channel 74 | NewlyCreated bool `json:"newly_created"` 75 | } 76 | 77 | // ThreadUpdate is the data for a ThreadUpdate event. 78 | type ThreadUpdate struct { 79 | *Channel 80 | BeforeUpdate *Channel `json:"-"` 81 | } 82 | 83 | // ThreadDelete is the data for a ThreadDelete event. 84 | type ThreadDelete struct { 85 | *Channel 86 | } 87 | 88 | // ThreadListSync is the data for a ThreadListSync event. 89 | type ThreadListSync struct { 90 | // The id of the guild 91 | GuildID string `json:"guild_id"` 92 | // The parent channel ids whose threads are being synced. 93 | // If omitted, then threads were synced for the entire guild. 94 | // This array may contain channel_ids that have no active threads as well, so you know to clear that data. 95 | ChannelIDs []string `json:"channel_ids"` 96 | // All active threads in the given channels that the current user can access 97 | Threads []*Channel `json:"threads"` 98 | // All thread member objects from the synced threads for the current user, 99 | // indicating which threads the current user has been added to 100 | Members []*ThreadMember `json:"members"` 101 | } 102 | 103 | // ThreadMemberUpdate is the data for a ThreadMemberUpdate event. 104 | type ThreadMemberUpdate struct { 105 | *ThreadMember 106 | GuildID string `json:"guild_id"` 107 | } 108 | 109 | // ThreadMembersUpdate is the data for a ThreadMembersUpdate event. 110 | type ThreadMembersUpdate struct { 111 | ID string `json:"id"` 112 | GuildID string `json:"guild_id"` 113 | MemberCount int `json:"member_count"` 114 | AddedMembers []AddedThreadMember `json:"added_members"` 115 | RemovedMembers []string `json:"removed_member_ids"` 116 | } 117 | 118 | // GuildCreate is the data for a GuildCreate event. 119 | type GuildCreate struct { 120 | *Guild 121 | } 122 | 123 | // GuildUpdate is the data for a GuildUpdate event. 124 | type GuildUpdate struct { 125 | *Guild 126 | } 127 | 128 | // GuildDelete is the data for a GuildDelete event. 129 | type GuildDelete struct { 130 | *Guild 131 | BeforeDelete *Guild `json:"-"` 132 | } 133 | 134 | // GuildBanAdd is the data for a GuildBanAdd event. 135 | type GuildBanAdd struct { 136 | User *User `json:"user"` 137 | GuildID string `json:"guild_id"` 138 | } 139 | 140 | // GuildBanRemove is the data for a GuildBanRemove event. 141 | type GuildBanRemove struct { 142 | User *User `json:"user"` 143 | GuildID string `json:"guild_id"` 144 | } 145 | 146 | // GuildMemberAdd is the data for a GuildMemberAdd event. 147 | type GuildMemberAdd struct { 148 | *Member 149 | } 150 | 151 | // GuildMemberUpdate is the data for a GuildMemberUpdate event. 152 | type GuildMemberUpdate struct { 153 | *Member 154 | BeforeUpdate *Member `json:"-"` 155 | } 156 | 157 | // GuildMemberRemove is the data for a GuildMemberRemove event. 158 | type GuildMemberRemove struct { 159 | *Member 160 | } 161 | 162 | // GuildRoleCreate is the data for a GuildRoleCreate event. 163 | type GuildRoleCreate struct { 164 | *GuildRole 165 | } 166 | 167 | // GuildRoleUpdate is the data for a GuildRoleUpdate event. 168 | type GuildRoleUpdate struct { 169 | *GuildRole 170 | } 171 | 172 | // A GuildRoleDelete is the data for a GuildRoleDelete event. 173 | type GuildRoleDelete struct { 174 | RoleID string `json:"role_id"` 175 | GuildID string `json:"guild_id"` 176 | } 177 | 178 | // A GuildEmojisUpdate is the data for a guild emoji update event. 179 | type GuildEmojisUpdate struct { 180 | GuildID string `json:"guild_id"` 181 | Emojis []*Emoji `json:"emojis"` 182 | } 183 | 184 | // A GuildStickersUpdate is the data for a GuildStickersUpdate event. 185 | type GuildStickersUpdate struct { 186 | GuildID string `json:"guild_id"` 187 | Stickers []*Sticker `json:"stickers"` 188 | } 189 | 190 | // A GuildMembersChunk is the data for a GuildMembersChunk event. 191 | type GuildMembersChunk struct { 192 | GuildID string `json:"guild_id"` 193 | Members []*Member `json:"members"` 194 | ChunkIndex int `json:"chunk_index"` 195 | ChunkCount int `json:"chunk_count"` 196 | NotFound []string `json:"not_found,omitempty"` 197 | Presences []*Presence `json:"presences,omitempty"` 198 | Nonce string `json:"nonce,omitempty"` 199 | } 200 | 201 | // GuildIntegrationsUpdate is the data for a GuildIntegrationsUpdate event. 202 | type GuildIntegrationsUpdate struct { 203 | GuildID string `json:"guild_id"` 204 | } 205 | 206 | // StageInstanceEventCreate is the data for a StageInstanceEventCreate event. 207 | type StageInstanceEventCreate struct { 208 | *StageInstance 209 | } 210 | 211 | // StageInstanceEventUpdate is the data for a StageInstanceEventUpdate event. 212 | type StageInstanceEventUpdate struct { 213 | *StageInstance 214 | } 215 | 216 | // StageInstanceEventDelete is the data for a StageInstanceEventDelete event. 217 | type StageInstanceEventDelete struct { 218 | *StageInstance 219 | } 220 | 221 | // GuildScheduledEventCreate is the data for a GuildScheduledEventCreate event. 222 | type GuildScheduledEventCreate struct { 223 | *GuildScheduledEvent 224 | } 225 | 226 | // GuildScheduledEventUpdate is the data for a GuildScheduledEventUpdate event. 227 | type GuildScheduledEventUpdate struct { 228 | *GuildScheduledEvent 229 | } 230 | 231 | // GuildScheduledEventDelete is the data for a GuildScheduledEventDelete event. 232 | type GuildScheduledEventDelete struct { 233 | *GuildScheduledEvent 234 | } 235 | 236 | // GuildScheduledEventUserAdd is the data for a GuildScheduledEventUserAdd event. 237 | type GuildScheduledEventUserAdd struct { 238 | GuildScheduledEventID string `json:"guild_scheduled_event_id"` 239 | UserID string `json:"user_id"` 240 | GuildID string `json:"guild_id"` 241 | } 242 | 243 | // GuildScheduledEventUserRemove is the data for a GuildScheduledEventUserRemove event. 244 | type GuildScheduledEventUserRemove struct { 245 | GuildScheduledEventID string `json:"guild_scheduled_event_id"` 246 | UserID string `json:"user_id"` 247 | GuildID string `json:"guild_id"` 248 | } 249 | 250 | // IntegrationCreate is the data for a IntegrationCreate event. 251 | type IntegrationCreate struct { 252 | *Integration 253 | GuildID string `json:"guild_id"` 254 | } 255 | 256 | // IntegrationUpdate is the data for a IntegrationUpdate event. 257 | type IntegrationUpdate struct { 258 | *Integration 259 | GuildID string `json:"guild_id"` 260 | } 261 | 262 | // IntegrationDelete is the data for a IntegrationDelete event. 263 | type IntegrationDelete struct { 264 | ID string `json:"id"` 265 | GuildID string `json:"guild_id"` 266 | ApplicationID string `json:"application_id,omitempty"` 267 | } 268 | 269 | // MessageCreate is the data for a MessageCreate event. 270 | type MessageCreate struct { 271 | *Message 272 | } 273 | 274 | // UnmarshalJSON is a helper function to unmarshal MessageCreate object. 275 | func (m *MessageCreate) UnmarshalJSON(b []byte) error { 276 | return json.Unmarshal(b, &m.Message) 277 | } 278 | 279 | // MessageUpdate is the data for a MessageUpdate event. 280 | type MessageUpdate struct { 281 | *Message 282 | // BeforeUpdate will be nil if the Message was not previously cached in the state cache. 283 | BeforeUpdate *Message `json:"-"` 284 | } 285 | 286 | // UnmarshalJSON is a helper function to unmarshal MessageUpdate object. 287 | func (m *MessageUpdate) UnmarshalJSON(b []byte) error { 288 | return json.Unmarshal(b, &m.Message) 289 | } 290 | 291 | // MessageDelete is the data for a MessageDelete event. 292 | type MessageDelete struct { 293 | *Message 294 | BeforeDelete *Message `json:"-"` 295 | } 296 | 297 | // UnmarshalJSON is a helper function to unmarshal MessageDelete object. 298 | func (m *MessageDelete) UnmarshalJSON(b []byte) error { 299 | return json.Unmarshal(b, &m.Message) 300 | } 301 | 302 | // MessageReactionAdd is the data for a MessageReactionAdd event. 303 | type MessageReactionAdd struct { 304 | *MessageReaction 305 | Member *Member `json:"member,omitempty"` 306 | } 307 | 308 | // MessageReactionRemove is the data for a MessageReactionRemove event. 309 | type MessageReactionRemove struct { 310 | *MessageReaction 311 | } 312 | 313 | // MessageReactionRemoveAll is the data for a MessageReactionRemoveAll event. 314 | type MessageReactionRemoveAll struct { 315 | *MessageReaction 316 | } 317 | 318 | // PresencesReplace is the data for a PresencesReplace event. 319 | type PresencesReplace []*Presence 320 | 321 | // PresenceUpdate is the data for a PresenceUpdate event. 322 | type PresenceUpdate struct { 323 | Presence 324 | GuildID string `json:"guild_id"` 325 | } 326 | 327 | // Resumed is the data for a Resumed event. 328 | type Resumed struct { 329 | Trace []string `json:"_trace"` 330 | } 331 | 332 | // TypingStart is the data for a TypingStart event. 333 | type TypingStart struct { 334 | UserID string `json:"user_id"` 335 | ChannelID string `json:"channel_id"` 336 | GuildID string `json:"guild_id,omitempty"` 337 | Timestamp int `json:"timestamp"` 338 | } 339 | 340 | // UserUpdate is the data for a UserUpdate event. 341 | type UserUpdate struct { 342 | *User 343 | } 344 | 345 | // VoiceServerUpdate is the data for a VoiceServerUpdate event. 346 | type VoiceServerUpdate struct { 347 | Token string `json:"token"` 348 | GuildID string `json:"guild_id"` 349 | Endpoint string `json:"endpoint"` 350 | } 351 | 352 | // VoiceStateUpdate is the data for a VoiceStateUpdate event. 353 | type VoiceStateUpdate struct { 354 | *VoiceState 355 | // BeforeUpdate will be nil if the VoiceState was not previously cached in the state cache. 356 | BeforeUpdate *VoiceState `json:"-"` 357 | } 358 | 359 | // MessageDeleteBulk is the data for a MessageDeleteBulk event 360 | type MessageDeleteBulk struct { 361 | Messages []string `json:"ids"` 362 | ChannelID string `json:"channel_id"` 363 | GuildID string `json:"guild_id"` 364 | } 365 | 366 | // WebhooksUpdate is the data for a WebhooksUpdate event 367 | type WebhooksUpdate struct { 368 | GuildID string `json:"guild_id"` 369 | ChannelID string `json:"channel_id"` 370 | } 371 | 372 | // InteractionCreate is the data for a InteractionCreate event 373 | type InteractionCreate struct { 374 | *Interaction 375 | } 376 | 377 | // UnmarshalJSON is a helper function to unmarshal Interaction object. 378 | func (i *InteractionCreate) UnmarshalJSON(b []byte) error { 379 | return json.Unmarshal(b, &i.Interaction) 380 | } 381 | 382 | // InviteCreate is the data for a InviteCreate event 383 | type InviteCreate struct { 384 | *Invite 385 | ChannelID string `json:"channel_id"` 386 | GuildID string `json:"guild_id"` 387 | } 388 | 389 | // InviteDelete is the data for a InviteDelete event 390 | type InviteDelete struct { 391 | ChannelID string `json:"channel_id"` 392 | GuildID string `json:"guild_id"` 393 | Code string `json:"code"` 394 | } 395 | 396 | // ApplicationCommandPermissionsUpdate is the data for an ApplicationCommandPermissionsUpdate event 397 | type ApplicationCommandPermissionsUpdate struct { 398 | *GuildApplicationCommandPermissions 399 | } 400 | 401 | // AutoModerationRuleCreate is the data for an AutoModerationRuleCreate event. 402 | type AutoModerationRuleCreate struct { 403 | *AutoModerationRule 404 | } 405 | 406 | // AutoModerationRuleUpdate is the data for an AutoModerationRuleUpdate event. 407 | type AutoModerationRuleUpdate struct { 408 | *AutoModerationRule 409 | } 410 | 411 | // AutoModerationRuleDelete is the data for an AutoModerationRuleDelete event. 412 | type AutoModerationRuleDelete struct { 413 | *AutoModerationRule 414 | } 415 | 416 | // AutoModerationActionExecution is the data for an AutoModerationActionExecution event. 417 | type AutoModerationActionExecution struct { 418 | GuildID string `json:"guild_id"` 419 | Action AutoModerationAction `json:"action"` 420 | RuleID string `json:"rule_id"` 421 | RuleTriggerType AutoModerationRuleTriggerType `json:"rule_trigger_type"` 422 | UserID string `json:"user_id"` 423 | ChannelID string `json:"channel_id"` 424 | MessageID string `json:"message_id"` 425 | AlertSystemMessageID string `json:"alert_system_message_id"` 426 | Content string `json:"content"` 427 | MatchedKeyword string `json:"matched_keyword"` 428 | MatchedContent string `json:"matched_content"` 429 | } 430 | 431 | // GuildAuditLogEntryCreate is the data for a GuildAuditLogEntryCreate event. 432 | type GuildAuditLogEntryCreate struct { 433 | *AuditLogEntry 434 | GuildID string `json:"guild_id"` 435 | } 436 | 437 | // MessagePollVoteAdd is the data for a MessagePollVoteAdd event. 438 | type MessagePollVoteAdd struct { 439 | UserID string `json:"user_id"` 440 | ChannelID string `json:"channel_id"` 441 | MessageID string `json:"message_id"` 442 | GuildID string `json:"guild_id,omitempty"` 443 | AnswerID int `json:"answer_id"` 444 | } 445 | 446 | // MessagePollVoteRemove is the data for a MessagePollVoteRemove event. 447 | type MessagePollVoteRemove struct { 448 | UserID string `json:"user_id"` 449 | ChannelID string `json:"channel_id"` 450 | MessageID string `json:"message_id"` 451 | GuildID string `json:"guild_id,omitempty"` 452 | AnswerID int `json:"answer_id"` 453 | } 454 | 455 | // EntitlementCreate is the data for an EntitlementCreate event. 456 | type EntitlementCreate struct { 457 | *Entitlement 458 | } 459 | 460 | // EntitlementUpdate is the data for an EntitlementUpdate event. 461 | type EntitlementUpdate struct { 462 | *Entitlement 463 | } 464 | 465 | // EntitlementDelete is the data for an EntitlementDelete event. 466 | // NOTE: Entitlements are not deleted when they expire. 467 | type EntitlementDelete struct { 468 | *Entitlement 469 | } 470 | 471 | // SubscriptionCreate is the data for an SubscriptionCreate event. 472 | // https://discord.com/developers/docs/monetization/implementing-app-subscriptions#using-subscription-events-for-the-subscription-lifecycle 473 | type SubscriptionCreate struct { 474 | *Subscription 475 | } 476 | 477 | // SubscriptionUpdate is the data for an SubscriptionUpdate event. 478 | // https://discord.com/developers/docs/monetization/implementing-app-subscriptions#using-subscription-events-for-the-subscription-lifecycle 479 | type SubscriptionUpdate struct { 480 | *Subscription 481 | } 482 | 483 | // SubscriptionDelete is the data for an SubscriptionDelete event. 484 | // https://discord.com/developers/docs/monetization/implementing-app-subscriptions#using-subscription-events-for-the-subscription-lifecycle 485 | type SubscriptionDelete struct { 486 | *Subscription 487 | } 488 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # DiscordGo Examples 4 | 5 | These examples demonstrate how to utilize DiscordGo. 6 | 7 | Please explore the individual folders and give them a try! 8 | 9 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 10 | Discord chat channel for support.** 11 | 12 | -------------------------------------------------------------------------------- /examples/airhorn/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Airhorn Example 4 | 5 | This example demonstrates how to utilize DiscordGo to listen for an !airhorn 6 | command in a channel and then play a sound to that user's current voice channel. 7 | 8 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 9 | Discord chat channel for support.** 10 | 11 | ### Build 12 | 13 | This assumes you already have a working Go environment setup and that 14 | DiscordGo is correctly installed on your system. 15 | 16 | From within the airhorn example folder, run the below command to compile the 17 | example. 18 | 19 | ```sh 20 | go build 21 | ``` 22 | 23 | ### Usage 24 | 25 | ``` 26 | Usage of ./airhorn: 27 | -t string 28 | Bot Token 29 | ``` 30 | 31 | The below example shows how to start the bot from the airhorn example folder. 32 | 33 | ```sh 34 | ./airhorn -t YOUR_BOT_TOKEN 35 | ``` 36 | 37 | ### Creating sounds 38 | 39 | Airhorn bot uses [DCA](https://github.com/bwmarrin/dca) files, which are 40 | pre-computed files that are easy to send to Discord. 41 | 42 | 43 | See the below example of creating a DCA file from a MP3 file. This also works 44 | with WAV, FLAC, and many other file formats. Of course, you will need to [install](https://github.com/bwmarrin/dca/tree/master/cmd/dca#Getting-Started) 45 | FFmpeg and the DCA CLI first. 46 | 47 | ```sh 48 | ffmpeg -i test.mp3 -f s16le -ar 48000 -ac 2 pipe:1 | dca > test.dca 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/airhorn/airhorn.dca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwmarrin/discordgo/6e8fa27c7917ea54d8b9ec26f126becae59058d2/examples/airhorn/airhorn.dca -------------------------------------------------------------------------------- /examples/airhorn/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/bwmarrin/discordgo" 15 | ) 16 | 17 | func init() { 18 | flag.StringVar(&token, "t", "", "Bot Token") 19 | flag.Parse() 20 | } 21 | 22 | var token string 23 | var buffer = make([][]byte, 0) 24 | 25 | func main() { 26 | 27 | if token == "" { 28 | fmt.Println("No token provided. Please run: airhorn -t ") 29 | return 30 | } 31 | 32 | // Load the sound file. 33 | err := loadSound() 34 | if err != nil { 35 | fmt.Println("Error loading sound: ", err) 36 | fmt.Println("Please copy $GOPATH/src/github.com/bwmarrin/examples/airhorn/airhorn.dca to this directory.") 37 | return 38 | } 39 | 40 | // Create a new Discord session using the provided bot token. 41 | dg, err := discordgo.New("Bot " + token) 42 | if err != nil { 43 | fmt.Println("Error creating Discord session: ", err) 44 | return 45 | } 46 | 47 | // Register ready as a callback for the ready events. 48 | dg.AddHandler(ready) 49 | 50 | // Register messageCreate as a callback for the messageCreate events. 51 | dg.AddHandler(messageCreate) 52 | 53 | // Register guildCreate as a callback for the guildCreate events. 54 | dg.AddHandler(guildCreate) 55 | 56 | // We need information about guilds (which includes their channels), 57 | // messages and voice states. 58 | dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages | discordgo.IntentsGuildVoiceStates 59 | 60 | // Open the websocket and begin listening. 61 | err = dg.Open() 62 | if err != nil { 63 | fmt.Println("Error opening Discord session: ", err) 64 | } 65 | 66 | // Wait here until CTRL-C or other term signal is received. 67 | fmt.Println("Airhorn is now running. Press CTRL-C to exit.") 68 | sc := make(chan os.Signal, 1) 69 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 70 | <-sc 71 | 72 | // Cleanly close down the Discord session. 73 | dg.Close() 74 | } 75 | 76 | // This function will be called (due to AddHandler above) when the bot receives 77 | // the "ready" event from Discord. 78 | func ready(s *discordgo.Session, event *discordgo.Ready) { 79 | 80 | // Set the playing status. 81 | s.UpdateGameStatus(0, "!airhorn") 82 | } 83 | 84 | // This function will be called (due to AddHandler above) every time a new 85 | // message is created on any channel that the autenticated bot has access to. 86 | func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 87 | 88 | // Ignore all messages created by the bot itself 89 | // This isn't required in this specific example but it's a good practice. 90 | if m.Author.ID == s.State.User.ID { 91 | return 92 | } 93 | 94 | // check if the message is "!airhorn" 95 | if strings.HasPrefix(m.Content, "!airhorn") { 96 | 97 | // Find the channel that the message came from. 98 | c, err := s.State.Channel(m.ChannelID) 99 | if err != nil { 100 | // Could not find channel. 101 | return 102 | } 103 | 104 | // Find the guild for that channel. 105 | g, err := s.State.Guild(c.GuildID) 106 | if err != nil { 107 | // Could not find guild. 108 | return 109 | } 110 | 111 | // Look for the message sender in that guild's current voice states. 112 | for _, vs := range g.VoiceStates { 113 | if vs.UserID == m.Author.ID { 114 | err = playSound(s, g.ID, vs.ChannelID) 115 | if err != nil { 116 | fmt.Println("Error playing sound:", err) 117 | } 118 | 119 | return 120 | } 121 | } 122 | } 123 | } 124 | 125 | // This function will be called (due to AddHandler above) every time a new 126 | // guild is joined. 127 | func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { 128 | 129 | if event.Guild.Unavailable { 130 | return 131 | } 132 | 133 | for _, channel := range event.Guild.Channels { 134 | if channel.ID == event.Guild.ID { 135 | _, _ = s.ChannelMessageSend(channel.ID, "Airhorn is ready! Type !airhorn while in a voice channel to play a sound.") 136 | return 137 | } 138 | } 139 | } 140 | 141 | // loadSound attempts to load an encoded sound file from disk. 142 | func loadSound() error { 143 | 144 | file, err := os.Open("airhorn.dca") 145 | if err != nil { 146 | fmt.Println("Error opening dca file :", err) 147 | return err 148 | } 149 | 150 | var opuslen int16 151 | 152 | for { 153 | // Read opus frame length from dca file. 154 | err = binary.Read(file, binary.LittleEndian, &opuslen) 155 | 156 | // If this is the end of the file, just return. 157 | if err == io.EOF || err == io.ErrUnexpectedEOF { 158 | err := file.Close() 159 | if err != nil { 160 | return err 161 | } 162 | return nil 163 | } 164 | 165 | if err != nil { 166 | fmt.Println("Error reading from dca file :", err) 167 | return err 168 | } 169 | 170 | // Read encoded pcm from dca file. 171 | InBuf := make([]byte, opuslen) 172 | err = binary.Read(file, binary.LittleEndian, &InBuf) 173 | 174 | // Should not be any end of file errors 175 | if err != nil { 176 | fmt.Println("Error reading from dca file :", err) 177 | return err 178 | } 179 | 180 | // Append encoded pcm data to the buffer. 181 | buffer = append(buffer, InBuf) 182 | } 183 | } 184 | 185 | // playSound plays the current buffer to the provided channel. 186 | func playSound(s *discordgo.Session, guildID, channelID string) (err error) { 187 | 188 | // Join the provided voice channel. 189 | vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | // Sleep for a specified amount of time before playing the sound 195 | time.Sleep(250 * time.Millisecond) 196 | 197 | // Start speaking. 198 | vc.Speaking(true) 199 | 200 | // Send the buffer data. 201 | for _, buff := range buffer { 202 | vc.OpusSend <- buff 203 | } 204 | 205 | // Stop speaking 206 | vc.Speaking(false) 207 | 208 | // Sleep for a specificed amount of time before ending. 209 | time.Sleep(250 * time.Millisecond) 210 | 211 | // Disconnect from the provided voice channel. 212 | vc.Disconnect() 213 | 214 | return nil 215 | } 216 | -------------------------------------------------------------------------------- /examples/auto_moderation/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Auto Moderation Example 4 | 5 | This example demonstrates how to utilize DiscordGo to manage auto moderation 6 | rules and triggers. 7 | 8 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 9 | Discord chat channel for support.** 10 | 11 | ### Build 12 | 13 | This assumes you already have a working Go environment setup and that 14 | DiscordGo is correctly installed on your system. 15 | 16 | From within the auto_moderation example folder, run the below command to compile the 17 | example. 18 | 19 | ```sh 20 | go build 21 | ``` 22 | 23 | ### Usage 24 | 25 | ``` 26 | Usage of auto_moderation: 27 | -channel string 28 | ID of the testing channel 29 | -guild string 30 | ID of the testing guild 31 | -token string 32 | Bot authorization token 33 | ``` 34 | 35 | The below example shows how to start the bot from the auto_moderation example folder. 36 | 37 | ```sh 38 | ./auto_moderation -channel YOUR_TESTING_CHANNEL -guild YOUR_TESTING_GUILD -token YOUR_BOT_TOKEN 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/auto_moderation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | // Command line flags 15 | var ( 16 | BotToken = flag.String("token", "", "Bot authorization token") 17 | GuildID = flag.String("guild", "", "ID of the testing guild") 18 | ChannelID = flag.String("channel", "", "ID of the testing channel") 19 | ) 20 | 21 | func init() { flag.Parse() } 22 | 23 | func main() { 24 | session, _ := discordgo.New("Bot " + *BotToken) 25 | session.Identify.Intents |= discordgo.IntentAutoModerationExecution 26 | session.Identify.Intents |= discordgo.IntentMessageContent 27 | 28 | enabled := true 29 | rule, err := session.AutoModerationRuleCreate(*GuildID, &discordgo.AutoModerationRule{ 30 | Name: "Auto Moderation example", 31 | EventType: discordgo.AutoModerationEventMessageSend, 32 | TriggerType: discordgo.AutoModerationEventTriggerKeyword, 33 | TriggerMetadata: &discordgo.AutoModerationTriggerMetadata{ 34 | KeywordFilter: []string{"*cat*"}, 35 | RegexPatterns: []string{"(c|b)at"}, 36 | }, 37 | 38 | Enabled: &enabled, 39 | Actions: []discordgo.AutoModerationAction{ 40 | {Type: discordgo.AutoModerationRuleActionBlockMessage}, 41 | }, 42 | }) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | fmt.Println("Successfully created the rule") 48 | defer session.AutoModerationRuleDelete(*GuildID, rule.ID) 49 | 50 | session.AddHandlerOnce(func(s *discordgo.Session, e *discordgo.AutoModerationActionExecution) { 51 | _, err = session.AutoModerationRuleEdit(*GuildID, rule.ID, &discordgo.AutoModerationRule{ 52 | TriggerMetadata: &discordgo.AutoModerationTriggerMetadata{ 53 | KeywordFilter: []string{"cat"}, 54 | }, 55 | Actions: []discordgo.AutoModerationAction{ 56 | {Type: discordgo.AutoModerationRuleActionTimeout, Metadata: &discordgo.AutoModerationActionMetadata{Duration: 60}}, 57 | {Type: discordgo.AutoModerationRuleActionSendAlertMessage, Metadata: &discordgo.AutoModerationActionMetadata{ 58 | ChannelID: e.ChannelID, 59 | }}, 60 | }, 61 | }) 62 | if err != nil { 63 | session.AutoModerationRuleDelete(*GuildID, rule.ID) 64 | panic(err) 65 | } 66 | 67 | s.ChannelMessageSend(e.ChannelID, "Congratulations! You have just triggered an auto moderation rule.\n"+ 68 | "The current trigger can match anywhere in the word, so even if you write the trigger word as a part of another word, it will still match.\n"+ 69 | "The rule has now been changed, now the trigger matches only in the full words.\n"+ 70 | "Additionally, when you send a message, an alert will be sent to this channel and you will be **timed out** for a minute.\n") 71 | 72 | var counter int 73 | var counterMutex sync.Mutex 74 | session.AddHandler(func(s *discordgo.Session, e *discordgo.AutoModerationActionExecution) { 75 | action := "unknown" 76 | switch e.Action.Type { 77 | case discordgo.AutoModerationRuleActionBlockMessage: 78 | action = "block message" 79 | case discordgo.AutoModerationRuleActionSendAlertMessage: 80 | action = "send alert message into <#" + e.Action.Metadata.ChannelID + ">" 81 | case discordgo.AutoModerationRuleActionTimeout: 82 | action = "timeout" 83 | } 84 | 85 | counterMutex.Lock() 86 | counter++ 87 | if counter == 1 { 88 | counterMutex.Unlock() 89 | s.ChannelMessageSend(e.ChannelID, "Nothing has changed, right? "+ 90 | "Well, since separate gateway events are fired per each action (current is "+action+"), "+ 91 | "you'll see a second message about an action pop up soon") 92 | } else if counter == 2 { 93 | counterMutex.Unlock() 94 | s.ChannelMessageSend(e.ChannelID, "Now the second ("+action+") action got executed.") 95 | s.ChannelMessageSend(e.ChannelID, "And... you've made it! That's the end of the example.\n"+ 96 | "For more information about the automod and how to use it, "+ 97 | "you can visit the official Discord docs: https://discord.dev/resources/auto-moderation or ask in our server: https://discord.gg/6dzbuDpSWY", 98 | ) 99 | 100 | session.Close() 101 | session.AutoModerationRuleDelete(*GuildID, rule.ID) 102 | os.Exit(0) 103 | } 104 | }) 105 | }) 106 | 107 | err = session.Open() 108 | if err != nil { 109 | log.Fatalf("Cannot open the session: %v", err) 110 | } 111 | defer session.Close() 112 | 113 | stop := make(chan os.Signal, 1) 114 | signal.Notify(stop, os.Interrupt) 115 | <-stop 116 | log.Println("Graceful shutdown") 117 | 118 | } 119 | -------------------------------------------------------------------------------- /examples/autocomplete/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Slash Command Autocomplete Option Example 4 | 5 | This example demonstrates how to utilize DiscordGo to create and use 6 | autocomplete options in Slash Commands. As this example uses interactions, 7 | slash commands and slash command options, it is recommended to read 8 | `slash_commands` example before proceeding. 9 | 10 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 11 | Discord chat channel for support.** 12 | 13 | ### Build 14 | 15 | This assumes you already have a working Go environment setup and that 16 | DiscordGo is correctly installed on your system. 17 | 18 | From within the autocomplete example folder, run the below command to compile the 19 | example. 20 | 21 | ```sh 22 | go build 23 | ``` 24 | 25 | ### Usage 26 | 27 | ``` 28 | Usage of autocomplete: 29 | -guild string 30 | Test guild ID. If not passed - bot registers commands globally 31 | -rmcmd 32 | Whether to remove all commands after shutting down (default true) 33 | -token string 34 | Bot access token 35 | ``` 36 | 37 | The below example shows how to start the bot from the autocomplete example folder. 38 | 39 | ```sh 40 | ./autocomplete -guild YOUR_TESTING_GUILD -token YOUR_BOT_TOKEN 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/autocomplete/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | ) 12 | 13 | // Bot parameters 14 | var ( 15 | GuildID = flag.String("guild", "", "Test guild ID. If not passed - bot registers commands globally") 16 | BotToken = flag.String("token", "", "Bot access token") 17 | RemoveCommands = flag.Bool("rmcmd", true, "Remove all commands after shutdowning or not") 18 | ) 19 | 20 | var s *discordgo.Session 21 | 22 | func init() { flag.Parse() } 23 | 24 | func init() { 25 | var err error 26 | s, err = discordgo.New("Bot " + *BotToken) 27 | if err != nil { 28 | log.Fatalf("Invalid bot parameters: %v", err) 29 | } 30 | } 31 | 32 | var ( 33 | commands = []*discordgo.ApplicationCommand{ 34 | { 35 | Name: "single-autocomplete", 36 | Description: "Showcase of single autocomplete option", 37 | Type: discordgo.ChatApplicationCommand, 38 | Options: []*discordgo.ApplicationCommandOption{ 39 | { 40 | Name: "autocomplete-option", 41 | Description: "Autocomplete option", 42 | Type: discordgo.ApplicationCommandOptionString, 43 | Required: true, 44 | Autocomplete: true, 45 | }, 46 | }, 47 | }, 48 | { 49 | Name: "multi-autocomplete", 50 | Description: "Showcase of multiple autocomplete option", 51 | Type: discordgo.ChatApplicationCommand, 52 | Options: []*discordgo.ApplicationCommandOption{ 53 | { 54 | Name: "autocomplete-option-1", 55 | Description: "Autocomplete option 1", 56 | Type: discordgo.ApplicationCommandOptionString, 57 | Required: true, 58 | Autocomplete: true, 59 | }, 60 | { 61 | Name: "autocomplete-option-2", 62 | Description: "Autocomplete option 2", 63 | Type: discordgo.ApplicationCommandOptionString, 64 | Required: true, 65 | Autocomplete: true, 66 | }, 67 | }, 68 | }, 69 | } 70 | 71 | commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ 72 | "single-autocomplete": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 73 | switch i.Type { 74 | case discordgo.InteractionApplicationCommand: 75 | data := i.ApplicationCommandData() 76 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 77 | Type: discordgo.InteractionResponseChannelMessageWithSource, 78 | Data: &discordgo.InteractionResponseData{ 79 | Content: fmt.Sprintf( 80 | "You picked %q autocompletion", 81 | // Autocompleted options do not affect usual flow of handling application command. They are ordinary options at this stage 82 | data.Options[0].StringValue(), 83 | ), 84 | }, 85 | }) 86 | if err != nil { 87 | panic(err) 88 | } 89 | // Autocomplete options introduce a new interaction type (8) for returning custom autocomplete results. 90 | case discordgo.InteractionApplicationCommandAutocomplete: 91 | data := i.ApplicationCommandData() 92 | choices := []*discordgo.ApplicationCommandOptionChoice{ 93 | { 94 | Name: "Autocomplete", 95 | Value: "autocomplete", 96 | }, 97 | { 98 | Name: "Autocomplete is best!", 99 | Value: "autocomplete_is_best", 100 | }, 101 | { 102 | Name: "Choice 3", 103 | Value: "choice3", 104 | }, 105 | { 106 | Name: "Choice 4", 107 | Value: "choice4", 108 | }, 109 | { 110 | Name: "Choice 5", 111 | Value: "choice5", 112 | }, 113 | // And so on, up to 25 choices 114 | } 115 | 116 | if data.Options[0].StringValue() != "" { 117 | choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ 118 | Name: data.Options[0].StringValue(), // To get user input you just get value of the autocomplete option. 119 | Value: "choice_custom", 120 | }) 121 | } 122 | 123 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 124 | Type: discordgo.InteractionApplicationCommandAutocompleteResult, 125 | Data: &discordgo.InteractionResponseData{ 126 | Choices: choices, // This is basically the whole purpose of autocomplete interaction - return custom options to the user. 127 | }, 128 | }) 129 | if err != nil { 130 | panic(err) 131 | } 132 | } 133 | }, 134 | "multi-autocomplete": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 135 | switch i.Type { 136 | case discordgo.InteractionApplicationCommand: 137 | data := i.ApplicationCommandData() 138 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 139 | Type: discordgo.InteractionResponseChannelMessageWithSource, 140 | Data: &discordgo.InteractionResponseData{ 141 | Content: fmt.Sprintf( 142 | "Option 1: %s\nOption 2: %s", 143 | data.Options[0].StringValue(), 144 | data.Options[1].StringValue(), 145 | ), 146 | }, 147 | }) 148 | if err != nil { 149 | panic(err) 150 | } 151 | case discordgo.InteractionApplicationCommandAutocomplete: 152 | data := i.ApplicationCommandData() 153 | var choices []*discordgo.ApplicationCommandOptionChoice 154 | switch { 155 | // In this case there are multiple autocomplete options. The Focused field shows which option user is focused on. 156 | case data.Options[0].Focused: 157 | choices = []*discordgo.ApplicationCommandOptionChoice{ 158 | { 159 | Name: "Autocomplete 4 first option", 160 | Value: "autocomplete_default", 161 | }, 162 | { 163 | Name: "Choice 3", 164 | Value: "choice3", 165 | }, 166 | { 167 | Name: "Choice 4", 168 | Value: "choice4", 169 | }, 170 | { 171 | Name: "Choice 5", 172 | Value: "choice5", 173 | }, 174 | } 175 | if data.Options[0].StringValue() != "" { 176 | choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ 177 | Name: data.Options[0].StringValue(), 178 | Value: "choice_custom", 179 | }) 180 | } 181 | 182 | case data.Options[1].Focused: 183 | choices = []*discordgo.ApplicationCommandOptionChoice{ 184 | { 185 | Name: "Autocomplete 4 second option", 186 | Value: "autocomplete_1_default", 187 | }, 188 | { 189 | Name: "Choice 3.1", 190 | Value: "choice3_1", 191 | }, 192 | { 193 | Name: "Choice 4.1", 194 | Value: "choice4_1", 195 | }, 196 | { 197 | Name: "Choice 5.1", 198 | Value: "choice5_1", 199 | }, 200 | } 201 | if data.Options[1].StringValue() != "" { 202 | choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ 203 | Name: data.Options[1].StringValue(), 204 | Value: "choice_custom_2", 205 | }) 206 | } 207 | } 208 | 209 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 210 | Type: discordgo.InteractionApplicationCommandAutocompleteResult, 211 | Data: &discordgo.InteractionResponseData{ 212 | Choices: choices, 213 | }, 214 | }) 215 | if err != nil { 216 | panic(err) 217 | } 218 | } 219 | }, 220 | } 221 | ) 222 | 223 | func main() { 224 | s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Println("Bot is up!") }) 225 | s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { 226 | if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { 227 | h(s, i) 228 | } 229 | }) 230 | err := s.Open() 231 | if err != nil { 232 | log.Fatalf("Cannot open the session: %v", err) 233 | } 234 | defer s.Close() 235 | 236 | createdCommands, err := s.ApplicationCommandBulkOverwrite(s.State.User.ID, *GuildID, commands) 237 | 238 | if err != nil { 239 | log.Fatalf("Cannot register commands: %v", err) 240 | } 241 | 242 | stop := make(chan os.Signal, 1) 243 | signal.Notify(stop, os.Interrupt) 244 | <-stop 245 | log.Println("Gracefully shutting down") 246 | 247 | if *RemoveCommands { 248 | for _, cmd := range createdCommands { 249 | err := s.ApplicationCommandDelete(s.State.User.ID, *GuildID, cmd.ID) 250 | if err != nil { 251 | log.Fatalf("Cannot delete %q command: %v", cmd.Name, err) 252 | } 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /examples/avatar/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Avatar Example 4 | 5 | This example demonstrates how to utilize DiscordGo to change the avatar for 6 | a Discord account. This example works both with a local file or the URL of 7 | an image. 8 | 9 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 10 | Discord chat channel for support.** 11 | 12 | ### Build 13 | 14 | This assumes you already have a working Go environment setup and that 15 | DiscordGo is correctly installed on your system. 16 | 17 | From within the avatar example folder, run the below command to compile the 18 | example. 19 | 20 | ```sh 21 | go build 22 | ``` 23 | 24 | ### Usage 25 | 26 | This example uses bot tokens for authentication only. While email/password is 27 | supported by DiscordGo, it is not recommended to use them. 28 | 29 | ``` 30 | ./avatar --help 31 | Usage of ./avatar: 32 | -f string 33 | Avatar File Name 34 | -t string 35 | Bot Token 36 | -u string 37 | URL to the avatar image 38 | ``` 39 | 40 | The below example shows how to set your Avatar from a local file. 41 | 42 | ```sh 43 | ./avatar -t TOKEN -f avatar.png 44 | ``` 45 | The below example shows how to set your Avatar from a URL. 46 | 47 | ```sh 48 | ./avatar -t TOKEN -u https://raw.githubusercontent.com/bwmarrin/discordgo/master/docs/img/discordgo.svg 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/avatar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | // Variables used for command line parameters 15 | var ( 16 | Token string 17 | AvatarFile string 18 | AvatarURL string 19 | ) 20 | 21 | func init() { 22 | 23 | flag.StringVar(&Token, "t", "", "Bot Token") 24 | flag.StringVar(&AvatarFile, "f", "", "Avatar File Name") 25 | flag.StringVar(&AvatarURL, "u", "", "URL to the avatar image") 26 | flag.Parse() 27 | 28 | if Token == "" || (AvatarFile == "" && AvatarURL == "") { 29 | flag.Usage() 30 | os.Exit(1) 31 | } 32 | } 33 | 34 | func main() { 35 | 36 | // Create a new Discord session using the provided login information. 37 | dg, err := discordgo.New("Bot " + Token) 38 | if err != nil { 39 | fmt.Println("error creating Discord session,", err) 40 | return 41 | } 42 | 43 | // Declare these here so they can be used in the below two if blocks and 44 | // still carry over to the end of this function. 45 | var base64img string 46 | var contentType string 47 | 48 | // If we're using a URL link for the Avatar 49 | if AvatarURL != "" { 50 | 51 | resp, err := http.Get(AvatarURL) 52 | if err != nil { 53 | fmt.Println("Error retrieving the file, ", err) 54 | return 55 | } 56 | 57 | defer func() { 58 | _ = resp.Body.Close() 59 | }() 60 | 61 | img, err := ioutil.ReadAll(resp.Body) 62 | if err != nil { 63 | fmt.Println("Error reading the response, ", err) 64 | return 65 | } 66 | 67 | contentType = http.DetectContentType(img) 68 | base64img = base64.StdEncoding.EncodeToString(img) 69 | } 70 | 71 | // If we're using a local file for the Avatar 72 | if AvatarFile != "" { 73 | img, err := ioutil.ReadFile(AvatarFile) 74 | if err != nil { 75 | fmt.Println(err) 76 | } 77 | 78 | contentType = http.DetectContentType(img) 79 | base64img = base64.StdEncoding.EncodeToString(img) 80 | } 81 | 82 | // Now lets format our base64 image into the proper format Discord wants 83 | // and then call UserUpdate to set it as our user's Avatar. 84 | avatar := fmt.Sprintf("data:%s;base64,%s", contentType, base64img) 85 | _, err = dg.UserUpdate("", avatar, "") 86 | if err != nil { 87 | fmt.Println(err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/components/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Components Example 4 | 5 | This example demonstrates how to utilize DiscordGo to create and use message 6 | components, such as buttons and select menus. For usage of the text input 7 | component and modals, please refer to the `modals` example. 8 | 9 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 10 | Discord chat channel for support.** 11 | 12 | ### Build 13 | 14 | This assumes you already have a working Go environment setup and that 15 | DiscordGo is correctly installed on your system. 16 | 17 | From within the components example folder, run the below command to compile the 18 | example. 19 | 20 | ```sh 21 | go build 22 | ``` 23 | 24 | ### Usage 25 | 26 | ``` 27 | Usage of components: 28 | -app string 29 | Application ID 30 | -guild string 31 | Test guild ID 32 | -token string 33 | Bot access token 34 | ``` 35 | 36 | The below example shows how to start the bot from the components example folder. 37 | 38 | ```sh 39 | ./components -app YOUR_APPLICATION_ID -guild YOUR_TESTING_GUILD -token YOUR_BOT_TOKEN 40 | ``` 41 | -------------------------------------------------------------------------------- /examples/context_menus/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Context Menu Commands Example 4 | 5 | This example demonstrates how to utilize DiscordGo to create and use context 6 | menu commands. This example heavily relies on `slash_commands` example in 7 | command handling and registration, therefore it is recommended to be read 8 | before proceeding. 9 | 10 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 11 | Discord chat channel for support.** 12 | 13 | ### Build 14 | 15 | This assumes you already have a working Go environment setup and that 16 | DiscordGo is correctly installed on your system. 17 | 18 | From within the context_menus example folder, run the below command to compile the 19 | example. 20 | 21 | ```sh 22 | go build 23 | ``` 24 | 25 | ### Usage 26 | 27 | ``` 28 | Usage of context_menus: 29 | -app string 30 | Application ID 31 | -cleanup 32 | Cleanup of commands (default true) 33 | -guild string 34 | Test guild ID 35 | -token string 36 | Bot access token 37 | ``` 38 | 39 | The below example shows how to start the bot from the context_menus example folder. 40 | 41 | ```sh 42 | ./context_menus -app YOUR_APPLICATION_ID -guild YOUR_TESTING_GUILD -token YOUR_BOT_TOKEN 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/context_menus/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | // Bot parameters 15 | var ( 16 | GuildID = flag.String("guild", "", "Test guild ID") 17 | BotToken = flag.String("token", "", "Bot access token") 18 | AppID = flag.String("app", "", "Application ID") 19 | Cleanup = flag.Bool("cleanup", true, "Cleanup of commands") 20 | ) 21 | 22 | var s *discordgo.Session 23 | 24 | func init() { flag.Parse() } 25 | 26 | func init() { 27 | var err error 28 | s, err = discordgo.New("Bot " + *BotToken) 29 | if err != nil { 30 | log.Fatalf("Invalid bot parameters: %v", err) 31 | } 32 | } 33 | 34 | func searchLink(message, format, sep string) string { 35 | return fmt.Sprintf(format, strings.Join( 36 | strings.Split( 37 | message, 38 | " ", 39 | ), 40 | sep, 41 | )) 42 | } 43 | 44 | var ( 45 | commands = []discordgo.ApplicationCommand{ 46 | { 47 | Name: "rickroll-em", 48 | Type: discordgo.UserApplicationCommand, 49 | }, 50 | { 51 | Name: "google-it", 52 | Type: discordgo.MessageApplicationCommand, 53 | }, 54 | { 55 | Name: "stackoverflow-it", 56 | Type: discordgo.MessageApplicationCommand, 57 | }, 58 | { 59 | Name: "godoc-it", 60 | Type: discordgo.MessageApplicationCommand, 61 | }, 62 | { 63 | Name: "discordjs-it", 64 | Type: discordgo.MessageApplicationCommand, 65 | }, 66 | { 67 | Name: "discordpy-it", 68 | Type: discordgo.MessageApplicationCommand, 69 | }, 70 | } 71 | commandsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ 72 | "rickroll-em": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 73 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 74 | Type: discordgo.InteractionResponseChannelMessageWithSource, 75 | Data: &discordgo.InteractionResponseData{ 76 | Content: "Operation rickroll has begun", 77 | Flags: discordgo.MessageFlagsEphemeral, 78 | }, 79 | }) 80 | if err != nil { 81 | panic(err) 82 | } 83 | 84 | ch, err := s.UserChannelCreate( 85 | i.ApplicationCommandData().TargetID, 86 | ) 87 | if err != nil { 88 | _, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ 89 | Content: fmt.Sprintf("Mission failed. Cannot send a message to this user: %q", err.Error()), 90 | Flags: discordgo.MessageFlagsEphemeral, 91 | }) 92 | if err != nil { 93 | panic(err) 94 | } 95 | } 96 | _, err = s.ChannelMessageSend( 97 | ch.ID, 98 | fmt.Sprintf("%s sent you this: https://youtu.be/dQw4w9WgXcQ", i.Member.Mention()), 99 | ) 100 | if err != nil { 101 | panic(err) 102 | } 103 | }, 104 | "google-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 105 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 106 | Type: discordgo.InteractionResponseChannelMessageWithSource, 107 | Data: &discordgo.InteractionResponseData{ 108 | Content: searchLink( 109 | i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content, 110 | "https://google.com/search?q=%s", "+"), 111 | Flags: discordgo.MessageFlagsEphemeral, 112 | }, 113 | }) 114 | if err != nil { 115 | panic(err) 116 | } 117 | }, 118 | "stackoverflow-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 119 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 120 | Type: discordgo.InteractionResponseChannelMessageWithSource, 121 | Data: &discordgo.InteractionResponseData{ 122 | Content: searchLink( 123 | i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content, 124 | "https://stackoverflow.com/search?q=%s", "+"), 125 | Flags: discordgo.MessageFlagsEphemeral, 126 | }, 127 | }) 128 | if err != nil { 129 | panic(err) 130 | } 131 | }, 132 | "godoc-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 133 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 134 | Type: discordgo.InteractionResponseChannelMessageWithSource, 135 | Data: &discordgo.InteractionResponseData{ 136 | Content: searchLink( 137 | i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content, 138 | "https://pkg.go.dev/search?q=%s", "+"), 139 | Flags: discordgo.MessageFlagsEphemeral, 140 | }, 141 | }) 142 | if err != nil { 143 | panic(err) 144 | } 145 | }, 146 | "discordjs-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 147 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 148 | Type: discordgo.InteractionResponseChannelMessageWithSource, 149 | Data: &discordgo.InteractionResponseData{ 150 | Content: searchLink( 151 | i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content, 152 | "https://discord.js.org/#/docs/main/stable/search?query=%s", "+"), 153 | Flags: discordgo.MessageFlagsEphemeral, 154 | }, 155 | }) 156 | if err != nil { 157 | panic(err) 158 | } 159 | }, 160 | "discordpy-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 161 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 162 | Type: discordgo.InteractionResponseChannelMessageWithSource, 163 | Data: &discordgo.InteractionResponseData{ 164 | Content: searchLink( 165 | i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content, 166 | "https://discordpy.readthedocs.io/en/stable/search.html?q=%s", "+"), 167 | Flags: discordgo.MessageFlagsEphemeral, 168 | }, 169 | }) 170 | if err != nil { 171 | panic(err) 172 | } 173 | }, 174 | } 175 | ) 176 | 177 | func main() { 178 | s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { 179 | log.Println("Bot is up!") 180 | }) 181 | 182 | s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { 183 | if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok { 184 | h(s, i) 185 | } 186 | }) 187 | 188 | cmdIDs := make(map[string]string, len(commands)) 189 | 190 | for _, cmd := range commands { 191 | rcmd, err := s.ApplicationCommandCreate(*AppID, *GuildID, &cmd) 192 | if err != nil { 193 | log.Fatalf("Cannot create slash command %q: %v", cmd.Name, err) 194 | } 195 | 196 | cmdIDs[rcmd.ID] = rcmd.Name 197 | 198 | } 199 | 200 | err := s.Open() 201 | if err != nil { 202 | log.Fatalf("Cannot open the session: %v", err) 203 | } 204 | defer s.Close() 205 | 206 | stop := make(chan os.Signal, 1) 207 | signal.Notify(stop, os.Interrupt) 208 | <-stop 209 | log.Println("Graceful shutdown") 210 | 211 | if !*Cleanup { 212 | return 213 | } 214 | 215 | for id, name := range cmdIDs { 216 | err := s.ApplicationCommandDelete(*AppID, *GuildID, id) 217 | if err != nil { 218 | log.Fatalf("Cannot delete slash command %q: %v", name, err) 219 | } 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /examples/dm_pingpong/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Direct Message Ping Pong Example 4 | 5 | This example demonstrates how to utilize DiscordGo to create a Ping Pong Bot 6 | that sends the response through Direct Message. 7 | 8 | This Bot will respond to "ping" in any server it's in with "Pong!" in the 9 | sender's DM. 10 | 11 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 12 | Discord chat channel for support.** 13 | 14 | ### Build 15 | 16 | This assumes you already have a working Go environment setup and that 17 | DiscordGo is correctly installed on your system. 18 | 19 | From within the dm_pingpong example folder, run the below command to compile the 20 | example. 21 | 22 | ```sh 23 | go build 24 | ``` 25 | 26 | ### Usage 27 | 28 | This example uses bot tokens for authentication only. While user/password is 29 | supported by DiscordGo, it is not recommended for bots. 30 | 31 | ``` 32 | ./dm_pingpong --help 33 | Usage of ./dm_pingpong: 34 | -t string 35 | Bot Token 36 | ``` 37 | 38 | The below example shows how to start the bot 39 | 40 | ```sh 41 | ./dm_pingpong -t YOUR_BOT_TOKEN 42 | Bot is now running. Press CTRL-C to exit. 43 | ``` 44 | -------------------------------------------------------------------------------- /examples/dm_pingpong/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | ) 12 | 13 | // Variables used for command line parameters 14 | var ( 15 | Token string 16 | ) 17 | 18 | func init() { 19 | flag.StringVar(&Token, "t", "", "Bot Token") 20 | flag.Parse() 21 | } 22 | 23 | func main() { 24 | // Create a new Discord session using the provided bot token. 25 | dg, err := discordgo.New("Bot " + Token) 26 | if err != nil { 27 | fmt.Println("error creating Discord session,", err) 28 | return 29 | } 30 | 31 | // Register the messageCreate func as a callback for MessageCreate events. 32 | dg.AddHandler(messageCreate) 33 | 34 | // Just like the ping pong example, we only care about receiving message 35 | // events in this example. 36 | dg.Identify.Intents = discordgo.IntentsGuildMessages 37 | 38 | // Open a websocket connection to Discord and begin listening. 39 | err = dg.Open() 40 | if err != nil { 41 | fmt.Println("error opening connection,", err) 42 | return 43 | } 44 | 45 | // Wait here until CTRL-C or other term signal is received. 46 | fmt.Println("Bot is now running. Press CTRL-C to exit.") 47 | sc := make(chan os.Signal, 1) 48 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 49 | <-sc 50 | 51 | // Cleanly close down the Discord session. 52 | dg.Close() 53 | } 54 | 55 | // This function will be called (due to AddHandler above) every time a new 56 | // message is created on any channel that the authenticated bot has access to. 57 | // 58 | // It is called whenever a message is created but only when it's sent through a 59 | // server as we did not request IntentsDirectMessages. 60 | func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 61 | // Ignore all messages created by the bot itself 62 | // This isn't required in this specific example but it's a good practice. 63 | if m.Author.ID == s.State.User.ID { 64 | return 65 | } 66 | // In this example, we only care about messages that are "ping". 67 | if m.Content != "ping" { 68 | return 69 | } 70 | 71 | // We create the private channel with the user who sent the message. 72 | channel, err := s.UserChannelCreate(m.Author.ID) 73 | if err != nil { 74 | // If an error occurred, we failed to create the channel. 75 | // 76 | // Some common causes are: 77 | // 1. We don't share a server with the user (not possible here). 78 | // 2. We opened enough DM channels quickly enough for Discord to 79 | // label us as abusing the endpoint, blocking us from opening 80 | // new ones. 81 | fmt.Println("error creating channel:", err) 82 | s.ChannelMessageSend( 83 | m.ChannelID, 84 | "Something went wrong while sending the DM!", 85 | ) 86 | return 87 | } 88 | // Then we send the message through the channel we created. 89 | _, err = s.ChannelMessageSend(channel.ID, "Pong!") 90 | if err != nil { 91 | // If an error occurred, we failed to send the message. 92 | // 93 | // It may occur either when we do not share a server with the 94 | // user (highly unlikely as we just received a message) or 95 | // the user disabled DM in their settings (more likely). 96 | fmt.Println("error sending DM message:", err) 97 | s.ChannelMessageSend( 98 | m.ChannelID, 99 | "Failed to send you a DM. "+ 100 | "Did you disable DM in your privacy settings?", 101 | ) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/echo/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Echo Example 4 | 5 | This example demonstrates how to utilize DiscordGo to create a simple, 6 | slash commands based bot, that will echo your messages. 7 | 8 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 9 | Discord chat channel for support.** 10 | 11 | ### Build 12 | 13 | This assumes you already have a working Go environment setup and that 14 | DiscordGo is correctly installed on your system. 15 | 16 | From within the example folder, run the below command to compile the 17 | example. 18 | 19 | ```sh 20 | go build 21 | ``` 22 | 23 | ### Usage 24 | 25 | ``` 26 | Usage of echo: 27 | -app string 28 | Application ID 29 | -guild string 30 | Guild ID 31 | -token string 32 | Bot authentication token 33 | 34 | ``` 35 | 36 | Run the command below to start the bot. 37 | 38 | ```sh 39 | ./echo -guild YOUR_TESTING_GUILD -app YOUR_TESTING_APP -token YOUR_BOT_TOKEN 40 | ``` 41 | -------------------------------------------------------------------------------- /examples/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | ) 12 | 13 | type optionMap = map[string]*discordgo.ApplicationCommandInteractionDataOption 14 | 15 | func parseOptions(options []*discordgo.ApplicationCommandInteractionDataOption) (om optionMap) { 16 | om = make(optionMap) 17 | for _, opt := range options { 18 | om[opt.Name] = opt 19 | } 20 | return 21 | } 22 | 23 | func interactionAuthor(i *discordgo.Interaction) *discordgo.User { 24 | if i.Member != nil { 25 | return i.Member.User 26 | } 27 | return i.User 28 | } 29 | 30 | func handleEcho(s *discordgo.Session, i *discordgo.InteractionCreate, opts optionMap) { 31 | builder := new(strings.Builder) 32 | if v, ok := opts["author"]; ok && v.BoolValue() { 33 | author := interactionAuthor(i.Interaction) 34 | builder.WriteString("**" + author.String() + "** says: ") 35 | } 36 | builder.WriteString(opts["message"].StringValue()) 37 | 38 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 39 | Type: discordgo.InteractionResponseChannelMessageWithSource, 40 | Data: &discordgo.InteractionResponseData{ 41 | Content: builder.String(), 42 | }, 43 | }) 44 | 45 | if err != nil { 46 | log.Panicf("could not respond to interaction: %s", err) 47 | } 48 | } 49 | 50 | var commands = []*discordgo.ApplicationCommand{ 51 | { 52 | Name: "echo", 53 | Description: "Say something through a bot", 54 | Options: []*discordgo.ApplicationCommandOption{ 55 | { 56 | Name: "message", 57 | Description: "Contents of the message", 58 | Type: discordgo.ApplicationCommandOptionString, 59 | Required: true, 60 | }, 61 | { 62 | Name: "author", 63 | Description: "Whether to prepend message's author", 64 | Type: discordgo.ApplicationCommandOptionBoolean, 65 | }, 66 | }, 67 | }, 68 | } 69 | 70 | var ( 71 | Token = flag.String("token", "", "Bot authentication token") 72 | App = flag.String("app", "", "Application ID") 73 | Guild = flag.String("guild", "", "Guild ID") 74 | ) 75 | 76 | func main() { 77 | flag.Parse() 78 | if *App == "" { 79 | log.Fatal("application id is not set") 80 | } 81 | 82 | session, _ := discordgo.New("Bot " + *Token) 83 | 84 | session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { 85 | if i.Type != discordgo.InteractionApplicationCommand { 86 | return 87 | } 88 | 89 | data := i.ApplicationCommandData() 90 | if data.Name != "echo" { 91 | return 92 | } 93 | 94 | handleEcho(s, i, parseOptions(data.Options)) 95 | }) 96 | 97 | session.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { 98 | log.Printf("Logged in as %s", r.User.String()) 99 | }) 100 | 101 | _, err := session.ApplicationCommandBulkOverwrite(*App, *Guild, commands) 102 | if err != nil { 103 | log.Fatalf("could not register commands: %s", err) 104 | } 105 | 106 | err = session.Open() 107 | if err != nil { 108 | log.Fatalf("could not open session: %s", err) 109 | } 110 | 111 | sigch := make(chan os.Signal, 1) 112 | signal.Notify(sigch, os.Interrupt) 113 | <-sigch 114 | 115 | err = session.Close() 116 | if err != nil { 117 | log.Printf("could not close session gracefully: %s", err) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /examples/linked_roles/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bwmarrin/discordgo/examples/linked_roles 2 | 3 | go 1.13 4 | 5 | replace github.com/bwmarrin/discordgo v0.26.1 => ../../ 6 | 7 | require ( 8 | github.com/bwmarrin/discordgo v0.26.1 9 | github.com/joho/godotenv v1.4.0 10 | golang.org/x/oauth2 v0.3.0 11 | ) 12 | -------------------------------------------------------------------------------- /examples/linked_roles/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 2 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 3 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 4 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 5 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 6 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 8 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 10 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 11 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 12 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 13 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 15 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 16 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= 17 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 18 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 19 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 20 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 22 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 23 | golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= 24 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 25 | golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= 26 | golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= 27 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 35 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 37 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 38 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 41 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 42 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 43 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 46 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 47 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 48 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 49 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 50 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 51 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 52 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 53 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 54 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 55 | -------------------------------------------------------------------------------- /examples/linked_roles/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | "github.com/joho/godotenv" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | var oauthConfig = oauth2.Config{ 16 | Endpoint: oauth2.Endpoint{ 17 | AuthURL: "https://discord.com/oauth2/authorize", 18 | TokenURL: "https://discord.com/api/oauth2/token", 19 | }, 20 | Scopes: []string{"identify", "role_connections.write"}, 21 | } 22 | 23 | var ( 24 | appID = flag.String("app", "", "Application ID") 25 | token = flag.String("token", "", "Application token") 26 | clientSecret = flag.String("secret", "", "OAuth2 secret") 27 | redirectURL = flag.String("redirect", "", "OAuth2 Redirect URL") 28 | ) 29 | 30 | func init() { 31 | flag.Parse() 32 | godotenv.Load() 33 | oauthConfig.ClientID = *appID 34 | oauthConfig.ClientSecret = *clientSecret 35 | oauthConfig.RedirectURL, _ = url.JoinPath(*redirectURL, "/linked-roles-callback") 36 | } 37 | 38 | func main() { 39 | s, _ := discordgo.New("Bot " + *token) 40 | 41 | _, err := s.ApplicationRoleConnectionMetadataUpdate(*appID, []*discordgo.ApplicationRoleConnectionMetadata{ 42 | { 43 | Type: discordgo.ApplicationRoleConnectionMetadataIntegerGreaterThanOrEqual, 44 | Key: "loc", 45 | Name: "Lines of Code", 46 | NameLocalizations: map[discordgo.Locale]string{}, 47 | Description: "Total lines of code written", 48 | DescriptionLocalizations: map[discordgo.Locale]string{}, 49 | }, 50 | { 51 | Type: discordgo.ApplicationRoleConnectionMetadataBooleanEqual, 52 | Key: "gopher", 53 | Name: "Gopher", 54 | NameLocalizations: map[discordgo.Locale]string{}, 55 | Description: "Writes in Go", 56 | DescriptionLocalizations: map[discordgo.Locale]string{}, 57 | }, 58 | { 59 | Type: discordgo.ApplicationRoleConnectionMetadataDatetimeGreaterThanOrEqual, 60 | Key: "first_line", 61 | Name: "First line written", 62 | NameLocalizations: map[discordgo.Locale]string{}, 63 | Description: "Days since the first line of code", 64 | DescriptionLocalizations: map[discordgo.Locale]string{}, 65 | }, 66 | }) 67 | if err != nil { 68 | panic(err) 69 | } 70 | 71 | fmt.Println("Updated application metadata") 72 | http.HandleFunc("/linked-roles", func(w http.ResponseWriter, r *http.Request) { 73 | w.Header().Set("Cache-Control", "no-cache") 74 | // Redirect the user to Discord OAuth2 page. 75 | http.Redirect(w, r, oauthConfig.AuthCodeURL("random-state"), http.StatusMovedPermanently) 76 | }) 77 | http.HandleFunc("/linked-roles-callback", func(w http.ResponseWriter, r *http.Request) { 78 | q := r.URL.Query() 79 | // A safeguard against CSRF attacks. 80 | // Usually tied to requesting user or random. 81 | // NOTE: Hardcoded for the sake of the example. 82 | if q["state"][0] != "random-state" { 83 | return 84 | } 85 | 86 | // Fetch the tokens with code we've received. 87 | tokens, err := oauthConfig.Exchange(r.Context(), q["code"][0]) 88 | if err != nil { 89 | w.Write([]byte(err.Error())) 90 | return 91 | } 92 | 93 | // Construct a temporary session with user's OAuth2 access_token. 94 | ts, _ := discordgo.New("Bearer " + tokens.AccessToken) 95 | 96 | // Retrive the user data. 97 | u, err := ts.User("@me") 98 | if err != nil { 99 | w.Write([]byte(err.Error())) 100 | return 101 | } 102 | 103 | // Fetch external metadata... 104 | // NOTE: Hardcoded for the sake of the example. 105 | metadata := map[string]string{ 106 | "gopher": "1", // 1 for true, 0 for false 107 | "loc": "10000", 108 | "first_line": "1970-01-01", // YYYY-MM-DD 109 | } 110 | 111 | // And submit it back to discord. 112 | _, err = ts.UserApplicationRoleConnectionUpdate(*appID, &discordgo.ApplicationRoleConnection{ 113 | PlatformName: "Discord Gophers", 114 | PlatformUsername: u.Username, 115 | Metadata: metadata, 116 | }) 117 | if err != nil { 118 | w.Write([]byte(err.Error())) 119 | return 120 | } 121 | 122 | // Retrieve it to check if everything is ok. 123 | info, err := ts.UserApplicationRoleConnection(*appID) 124 | if err != nil { 125 | w.Write([]byte(err.Error())) 126 | return 127 | } 128 | jsonMetadata, _ := json.Marshal(info.Metadata) 129 | // And show it to the user. 130 | w.Write([]byte(fmt.Sprintf("Your updated metadata is: %s", jsonMetadata))) 131 | }) 132 | http.ListenAndServe(":8010", nil) 133 | } 134 | -------------------------------------------------------------------------------- /examples/modals/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Modals Example 4 | 5 | This example demonstrates how to utilize DiscordGo to send and process text 6 | inputs in modals. If you have not read `slash_commands` and `components` 7 | examples yet it is recommended to do so before proceeding. As this example 8 | is built using interactions and Slash Commands. 9 | 10 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 11 | Discord chat channel for support.** 12 | 13 | ### Build 14 | 15 | This assumes you already have a working Go environment setup and that 16 | DiscordGo is correctly installed on your system. 17 | 18 | From within the modals example folder, run the below command to compile the 19 | example. 20 | 21 | ```sh 22 | go build 23 | ``` 24 | 25 | ### Usage 26 | 27 | ``` 28 | Usage of modals: 29 | -app string 30 | Application ID 31 | -cleanup 32 | Cleanup of commands (default true) 33 | -guild string 34 | Test guild ID 35 | -results string 36 | Channel where send survey results to 37 | -token string 38 | Bot access token 39 | ``` 40 | 41 | The below example shows how to start the bot from the modals example folder. 42 | 43 | ```sh 44 | ./modals -app YOUR_APPLICATION_ID -guild YOUR_TESTING_GUILD -results YOUR_TESTING_CHANNEL -token YOUR_BOT_TOKEN 45 | ``` 46 | -------------------------------------------------------------------------------- /examples/modals/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | // Bot parameters 15 | var ( 16 | GuildID = flag.String("guild", "", "Test guild ID") 17 | BotToken = flag.String("token", "", "Bot access token") 18 | AppID = flag.String("app", "", "Application ID") 19 | Cleanup = flag.Bool("cleanup", true, "Cleanup of commands") 20 | ResultsChannel = flag.String("results", "", "Channel where send survey results to") 21 | ) 22 | 23 | var s *discordgo.Session 24 | 25 | func init() { 26 | flag.Parse() 27 | } 28 | 29 | func init() { 30 | var err error 31 | s, err = discordgo.New("Bot " + *BotToken) 32 | if err != nil { 33 | log.Fatalf("Invalid bot parameters: %v", err) 34 | } 35 | } 36 | 37 | var ( 38 | commands = []discordgo.ApplicationCommand{ 39 | { 40 | Name: "modals-survey", 41 | Description: "Take a survey about modals", 42 | }, 43 | } 44 | commandsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ 45 | "modals-survey": func(s *discordgo.Session, i *discordgo.InteractionCreate) { 46 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 47 | Type: discordgo.InteractionResponseModal, 48 | Data: &discordgo.InteractionResponseData{ 49 | CustomID: "modals_survey_" + i.Interaction.Member.User.ID, 50 | Title: "Modals survey", 51 | Components: []discordgo.MessageComponent{ 52 | discordgo.ActionsRow{ 53 | Components: []discordgo.MessageComponent{ 54 | discordgo.TextInput{ 55 | CustomID: "opinion", 56 | Label: "What is your opinion on them?", 57 | Style: discordgo.TextInputShort, 58 | Placeholder: "Don't be shy, share your opinion with us", 59 | Required: true, 60 | MaxLength: 300, 61 | MinLength: 10, 62 | }, 63 | }, 64 | }, 65 | discordgo.ActionsRow{ 66 | Components: []discordgo.MessageComponent{ 67 | discordgo.TextInput{ 68 | CustomID: "suggestions", 69 | Label: "What would you suggest to improve them?", 70 | Style: discordgo.TextInputParagraph, 71 | Required: false, 72 | MaxLength: 2000, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }) 79 | if err != nil { 80 | panic(err) 81 | } 82 | }, 83 | } 84 | ) 85 | 86 | func main() { 87 | s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { 88 | log.Println("Bot is up!") 89 | }) 90 | 91 | s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { 92 | switch i.Type { 93 | case discordgo.InteractionApplicationCommand: 94 | if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok { 95 | h(s, i) 96 | } 97 | case discordgo.InteractionModalSubmit: 98 | err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 99 | Type: discordgo.InteractionResponseChannelMessageWithSource, 100 | Data: &discordgo.InteractionResponseData{ 101 | Content: "Thank you for taking your time to fill this survey", 102 | Flags: discordgo.MessageFlagsEphemeral, 103 | }, 104 | }) 105 | if err != nil { 106 | panic(err) 107 | } 108 | data := i.ModalSubmitData() 109 | 110 | if !strings.HasPrefix(data.CustomID, "modals_survey") { 111 | return 112 | } 113 | 114 | userid := strings.Split(data.CustomID, "_")[2] 115 | _, err = s.ChannelMessageSend(*ResultsChannel, fmt.Sprintf( 116 | "Feedback received. From <@%s>\n\n**Opinion**:\n%s\n\n**Suggestions**:\n%s", 117 | userid, 118 | data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value, 119 | data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value, 120 | )) 121 | if err != nil { 122 | panic(err) 123 | } 124 | } 125 | }) 126 | 127 | cmdIDs := make(map[string]string, len(commands)) 128 | 129 | for _, cmd := range commands { 130 | rcmd, err := s.ApplicationCommandCreate(*AppID, *GuildID, &cmd) 131 | if err != nil { 132 | log.Fatalf("Cannot create slash command %q: %v", cmd.Name, err) 133 | } 134 | 135 | cmdIDs[rcmd.ID] = rcmd.Name 136 | } 137 | 138 | err := s.Open() 139 | if err != nil { 140 | log.Fatalf("Cannot open the session: %v", err) 141 | } 142 | defer s.Close() 143 | 144 | stop := make(chan os.Signal, 1) 145 | signal.Notify(stop, os.Interrupt) 146 | <-stop 147 | log.Println("Graceful shutdown") 148 | 149 | if !*Cleanup { 150 | return 151 | } 152 | 153 | for id, name := range cmdIDs { 154 | err := s.ApplicationCommandDelete(*AppID, *GuildID, id) 155 | if err != nil { 156 | log.Fatalf("Cannot delete slash command %q: %v", name, err) 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /examples/pingpong/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Ping Pong Example 4 | 5 | This example demonstrates how to utilize DiscordGo to create a Ping Pong Bot. 6 | 7 | This Bot will respond to "ping" with "Pong!" and "pong" with "Ping!". 8 | 9 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 10 | Discord chat channel for support.** 11 | 12 | ### Build 13 | 14 | This assumes you already have a working Go environment setup and that 15 | DiscordGo is correctly installed on your system. 16 | 17 | 18 | From within the pingpong example folder, run the below command to compile the 19 | example. 20 | 21 | ```sh 22 | go build 23 | ``` 24 | 25 | ### Usage 26 | 27 | This example uses bot tokens for authentication only. While user/password is 28 | supported by DiscordGo, it is not recommended for bots. 29 | 30 | ``` 31 | ./pingpong --help 32 | Usage of ./pingpong: 33 | -t string 34 | Bot Token 35 | ``` 36 | 37 | The below example shows how to start the bot 38 | 39 | ```sh 40 | ./pingpong -t YOUR_BOT_TOKEN 41 | Bot is now running. Press CTRL-C to exit. 42 | ``` 43 | -------------------------------------------------------------------------------- /examples/pingpong/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | ) 12 | 13 | // Variables used for command line parameters 14 | var ( 15 | Token string 16 | ) 17 | 18 | func init() { 19 | 20 | flag.StringVar(&Token, "t", "", "Bot Token") 21 | flag.Parse() 22 | } 23 | 24 | func main() { 25 | 26 | // Create a new Discord session using the provided bot token. 27 | dg, err := discordgo.New("Bot " + Token) 28 | if err != nil { 29 | fmt.Println("error creating Discord session,", err) 30 | return 31 | } 32 | 33 | // Register the messageCreate func as a callback for MessageCreate events. 34 | dg.AddHandler(messageCreate) 35 | 36 | // In this example, we only care about receiving message events. 37 | dg.Identify.Intents = discordgo.IntentsGuildMessages 38 | 39 | // Open a websocket connection to Discord and begin listening. 40 | err = dg.Open() 41 | if err != nil { 42 | fmt.Println("error opening connection,", err) 43 | return 44 | } 45 | 46 | // Wait here until CTRL-C or other term signal is received. 47 | fmt.Println("Bot is now running. Press CTRL-C to exit.") 48 | sc := make(chan os.Signal, 1) 49 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 50 | <-sc 51 | 52 | // Cleanly close down the Discord session. 53 | dg.Close() 54 | } 55 | 56 | // This function will be called (due to AddHandler above) every time a new 57 | // message is created on any channel that the authenticated bot has access to. 58 | func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 59 | 60 | // Ignore all messages created by the bot itself 61 | // This isn't required in this specific example but it's a good practice. 62 | if m.Author.ID == s.State.User.ID { 63 | return 64 | } 65 | // If the message is "ping" reply with "Pong!" 66 | if m.Content == "ping" { 67 | s.ChannelMessageSend(m.ChannelID, "Pong!") 68 | } 69 | 70 | // If the message is "pong" reply with "Ping!" 71 | if m.Content == "pong" { 72 | s.ChannelMessageSend(m.ChannelID, "Ping!") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/scheduled_events/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Scheduled Events Example 4 | 5 | This example demonstrates how to utilize DiscordGo to manage scheduled events 6 | in a guild. 7 | 8 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 9 | Discord chat channel for support.** 10 | 11 | ### Build 12 | 13 | This assumes you already have a working Go environment setup and that 14 | DiscordGo is correctly installed on your system. 15 | 16 | From within the scheduled_events example folder, run the below command to compile the 17 | example. 18 | 19 | ```sh 20 | go build 21 | ``` 22 | 23 | ### Usage 24 | 25 | ``` 26 | Usage of scheduled_events: 27 | -guild string 28 | Test guild ID 29 | -token string 30 | Bot token 31 | -voice string 32 | Test voice channel ID 33 | ``` 34 | 35 | The below example shows how to start the bot from the scheduled_events example folder. 36 | 37 | ```sh 38 | ./scheduled_events -guild YOUR_TESTING_GUILD -token YOUR_BOT_TOKEN -voice YOUR_TESTING_CHANNEL 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/scheduled_events/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "time" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | // Flags 15 | var ( 16 | GuildID = flag.String("guild", "", "Test guild ID") 17 | VoiceChannelID = flag.String("voice", "", "Test voice channel ID") 18 | BotToken = flag.String("token", "", "Bot token") 19 | ) 20 | 21 | func init() { flag.Parse() } 22 | 23 | func main() { 24 | s, _ := discordgo.New("Bot " + *BotToken) 25 | s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { 26 | fmt.Println("Bot is ready") 27 | }) 28 | 29 | err := s.Open() 30 | if err != nil { 31 | log.Fatalf("Cannot open the session: %v", err) 32 | } 33 | defer s.Close() 34 | 35 | event := createAmazingEvent(s) 36 | transformEventToExternalEvent(s, event) 37 | 38 | stop := make(chan os.Signal, 1) 39 | signal.Notify(stop, os.Interrupt) 40 | <-stop 41 | log.Println("Graceful shutdown") 42 | 43 | } 44 | 45 | // Create a new event on guild 46 | func createAmazingEvent(s *discordgo.Session) *discordgo.GuildScheduledEvent { 47 | // Define the starting time (must be in future) 48 | startingTime := time.Now().Add(1 * time.Hour) 49 | // Define the ending time (must be after starting time) 50 | endingTime := startingTime.Add(30 * time.Minute) 51 | // Create the event 52 | scheduledEvent, err := s.GuildScheduledEventCreate(*GuildID, &discordgo.GuildScheduledEventParams{ 53 | Name: "Amazing Event", 54 | Description: "This event will start in 1 hour and last 30 minutes", 55 | ScheduledStartTime: &startingTime, 56 | ScheduledEndTime: &endingTime, 57 | EntityType: discordgo.GuildScheduledEventEntityTypeVoice, 58 | ChannelID: *VoiceChannelID, 59 | PrivacyLevel: discordgo.GuildScheduledEventPrivacyLevelGuildOnly, 60 | }) 61 | if err != nil { 62 | log.Printf("Error creating scheduled event: %v", err) 63 | return nil 64 | } 65 | 66 | fmt.Println("Created scheduled event:", scheduledEvent.Name) 67 | return scheduledEvent 68 | } 69 | 70 | func transformEventToExternalEvent(s *discordgo.Session, event *discordgo.GuildScheduledEvent) { 71 | scheduledEvent, err := s.GuildScheduledEventEdit(*GuildID, event.ID, &discordgo.GuildScheduledEventParams{ 72 | Name: "Amazing Event @ Discord Website", 73 | EntityType: discordgo.GuildScheduledEventEntityTypeExternal, 74 | EntityMetadata: &discordgo.GuildScheduledEventEntityMetadata{ 75 | Location: "https://discord.com", 76 | }, 77 | }) 78 | if err != nil { 79 | log.Printf("Error during transformation of scheduled voice event into external event: %v", err) 80 | return 81 | } 82 | 83 | fmt.Println("Created scheduled event:", scheduledEvent.Name) 84 | } 85 | -------------------------------------------------------------------------------- /examples/slash_commands/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Slash Commands Example 4 | 5 | This example demonstrates how to utilize DiscordGo to create a Slash Command based bot, 6 | which would be able to listen and respond to interactions. This example covers all aspects 7 | of slash command interactions: options, choices, responses and followup messages. 8 | To avoid confusion, this example is more of a **step-by-step tutorial**, than a demonstration bot. 9 | 10 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 11 | Discord chat channel for support.** 12 | 13 | ### Build 14 | 15 | This assumes you already have a working Go environment setup and that 16 | DiscordGo is correctly installed on your system. 17 | 18 | From within the slash_commands example folder, run the below command to compile the 19 | example. 20 | 21 | ```sh 22 | go build 23 | ``` 24 | 25 | ### Usage 26 | 27 | ``` 28 | Usage of slash_commands: 29 | -guild string 30 | Test guild ID. If not passed - bot registers commands globally 31 | -rmcmd 32 | Whether to remove all commands after shutting down (default true) 33 | -token string 34 | Bot access token 35 | ``` 36 | 37 | The below example shows how to start the bot from the slash_commands example folder. 38 | 39 | ```sh 40 | ./slash_commands -guild YOUR_TESTING_GUILD -token YOUR_BOT_TOKEN 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/stage_instance/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Stage Instance Example 4 | 5 | This example demonstrates how to utilize DiscordGo to manage stage instances. 6 | 7 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 8 | Discord chat channel for support.** 9 | 10 | ### Build 11 | 12 | This assumes you already have a working Go environment setup and that 13 | DiscordGo is correctly installed on your system. 14 | 15 | From within the stage_instance example folder, run the below command to compile the 16 | example. 17 | 18 | ```sh 19 | go build 20 | ``` 21 | 22 | ### Usage 23 | 24 | ``` 25 | Usage of stage_instance: 26 | -guild string 27 | Test guild ID 28 | -stage string 29 | Test stage channel ID 30 | -token string 31 | Bot token 32 | ``` 33 | 34 | The below example shows how to start the bot from the stage_instance example folder. 35 | 36 | ```sh 37 | ./stage_instance -guild YOUR_TESTING_GUILD -stage STAGE_CHANNEL_ID -token YOUR_BOT_TOKEN 38 | ``` 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/stage_instance/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | ) 11 | 12 | // Flags 13 | var ( 14 | GuildID = flag.String("guild", "", "Test guild ID") 15 | StageChannelID = flag.String("stage", "", "Test stage channel ID") 16 | BotToken = flag.String("token", "", "Bot token") 17 | ) 18 | 19 | func init() { flag.Parse() } 20 | 21 | // To be correctly used, the bot needs to be in a guild. 22 | // All actions must be done on a stage channel event 23 | func main() { 24 | s, _ := discordgo.New("Bot " + *BotToken) 25 | s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { 26 | fmt.Println("Bot is ready") 27 | }) 28 | 29 | err := s.Open() 30 | if err != nil { 31 | log.Fatalf("Cannot open the session: %v", err) 32 | } 33 | defer s.Close() 34 | 35 | // Create a new Stage instance on the previous channel 36 | si, err := s.StageInstanceCreate(&discordgo.StageInstanceParams{ 37 | ChannelID: *StageChannelID, 38 | Topic: "Amazing topic", 39 | PrivacyLevel: discordgo.StageInstancePrivacyLevelGuildOnly, 40 | SendStartNotification: true, 41 | }) 42 | if err != nil { 43 | log.Fatalf("Cannot create stage instance: %v", err) 44 | } 45 | log.Printf("Stage Instance %s has been successfully created", si.Topic) 46 | 47 | // Edit the stage instance with a new Topic 48 | si, err = s.StageInstanceEdit(*StageChannelID, &discordgo.StageInstanceParams{ 49 | Topic: "New amazing topic", 50 | }) 51 | if err != nil { 52 | log.Fatalf("Cannot edit stage instance: %v", err) 53 | } 54 | log.Printf("Stage Instance %s has been successfully edited", si.Topic) 55 | 56 | time.Sleep(5 * time.Second) 57 | if err = s.StageInstanceDelete(*StageChannelID); err != nil { 58 | log.Fatalf("Cannot delete stage instance: %v", err) 59 | } 60 | log.Printf("Stage Instance %s has been successfully deleted", si.Topic) 61 | } 62 | -------------------------------------------------------------------------------- /examples/threads/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Threads Example 4 | 5 | This example demonstrates how to utilize DiscordGo to manage channel threads. 6 | 7 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 8 | Discord chat channel for support.** 9 | 10 | ### Build 11 | 12 | This assumes you already have a working Go environment setup and that 13 | DiscordGo is correctly installed on your system. 14 | 15 | From within the threads example folder, run the below command to compile the 16 | example. 17 | 18 | ```sh 19 | go build 20 | ``` 21 | 22 | ### Usage 23 | 24 | ``` 25 | Usage of threads: 26 | -token string 27 | Bot token 28 | ``` 29 | 30 | The below example shows how to start the bot from the threads example folder. 31 | 32 | ```sh 33 | ./threads -token YOUR_BOT_TOKEN 34 | ``` 35 | -------------------------------------------------------------------------------- /examples/threads/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "time" 11 | 12 | "github.com/bwmarrin/discordgo" 13 | ) 14 | 15 | // Flags 16 | var ( 17 | BotToken = flag.String("token", "", "Bot token") 18 | ) 19 | 20 | const timeout time.Duration = time.Second * 10 21 | 22 | var games map[string]time.Time = make(map[string]time.Time) 23 | 24 | func init() { flag.Parse() } 25 | 26 | func main() { 27 | s, _ := discordgo.New("Bot " + *BotToken) 28 | s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { 29 | fmt.Println("Bot is ready") 30 | }) 31 | s.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { 32 | if strings.Contains(m.Content, "ping") { 33 | if ch, err := s.State.Channel(m.ChannelID); err != nil || !ch.IsThread() { 34 | thread, err := s.MessageThreadStartComplex(m.ChannelID, m.ID, &discordgo.ThreadStart{ 35 | Name: "Pong game with " + m.Author.Username, 36 | AutoArchiveDuration: 60, 37 | Invitable: false, 38 | RateLimitPerUser: 10, 39 | }) 40 | if err != nil { 41 | panic(err) 42 | } 43 | _, _ = s.ChannelMessageSend(thread.ID, "pong") 44 | m.ChannelID = thread.ID 45 | } else { 46 | _, _ = s.ChannelMessageSendReply(m.ChannelID, "pong", m.Reference()) 47 | } 48 | games[m.ChannelID] = time.Now() 49 | <-time.After(timeout) 50 | if time.Since(games[m.ChannelID]) >= timeout { 51 | archived := true 52 | locked := true 53 | _, err := s.ChannelEditComplex(m.ChannelID, &discordgo.ChannelEdit{ 54 | Archived: &archived, 55 | Locked: &locked, 56 | }) 57 | if err != nil { 58 | panic(err) 59 | } 60 | } 61 | } 62 | }) 63 | s.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged) 64 | 65 | err := s.Open() 66 | if err != nil { 67 | log.Fatalf("Cannot open the session: %v", err) 68 | } 69 | defer s.Close() 70 | 71 | stop := make(chan os.Signal, 1) 72 | signal.Notify(stop, os.Interrupt) 73 | <-stop 74 | log.Println("Graceful shutdown") 75 | 76 | } 77 | -------------------------------------------------------------------------------- /examples/voice_receive/README.md: -------------------------------------------------------------------------------- 1 | DiscordGo logo 2 | 3 | ## DiscordGo Voice Receive Example 4 | 5 | This example experiments with receiving voice data from Discord. It joins 6 | a specified voice channel, listens for 10 seconds and saves .ogg files for each 7 | SSRC that it finds in the channel. An exercise left to the reader is to translate 8 | these SSRCs to user IDs; see speaking update events for this information. :) 9 | 10 | This example makes heavy use of the [Pion](https://github.com/pion) family of libraries. 11 | Go check them out for anything to do with voice, video or WebRTC; it's a great 12 | group of people maintaining the project! 13 | 14 | Please note that voice receive is **not** officially supported, any may break 15 | at essentially any time (and has in the past). This code works at the time of 16 | its writing, but YMMV in the future. 17 | 18 | **Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) 19 | Discord chat channel for support.** 20 | 21 | ### Build 22 | 23 | To build, make sure that modules are enabled, and run: 24 | 25 | ```sh 26 | go build 27 | ``` 28 | 29 | ### Usage 30 | 31 | Three flags are required: the bot's token, the guild ID containing the voice channel to join, and the ID of the voice channel to join. 32 | 33 | ```sh 34 | ./voice_receive -t MY_TOKEN -g 1234123412341234 -c 5678567856785678 35 | ``` 36 | -------------------------------------------------------------------------------- /examples/voice_receive/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bwmarrin/discordgo/examples/voice_receive 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.28.1 7 | github.com/pion/rtp v1.6.0 8 | github.com/pion/webrtc/v3 v3.0.0-20200721060053-ca3cc9d940bc 9 | ) 10 | 11 | require ( 12 | github.com/gorilla/websocket v1.4.2 // indirect 13 | github.com/pion/randutil v0.1.0 // indirect 14 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect 15 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /examples/voice_receive/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= 2 | github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 3 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 9 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 10 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 12 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 13 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 14 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 15 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= 18 | github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= 19 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 20 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 21 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 22 | github.com/pion/datachannel v1.4.19/go.mod h1:JzKF/zzeWgkOYwQ+KFb8JzbrUt8s63um+Qunu8VqTyw= 23 | github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I= 24 | github.com/pion/ice/v2 v2.0.0-rc.8/go.mod h1:BH71LWfapOO69LRT1b+Rg9/be1qQPjSZT9Pm3qQRBdY= 25 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 26 | github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= 27 | github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= 28 | github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 29 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 30 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 31 | github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I= 32 | github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk= 33 | github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI= 34 | github.com/pion/sctp v1.7.8/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= 35 | github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= 36 | github.com/pion/srtp v1.5.0/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA= 37 | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= 38 | github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= 39 | github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= 40 | github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= 41 | github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= 42 | github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= 43 | github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= 44 | github.com/pion/webrtc/v3 v3.0.0-20200721060053-ca3cc9d940bc h1:MEXLzhmwidVyYVKJ+Z+b+eumAc3Pz8yrms26RPHF4J4= 45 | github.com/pion/webrtc/v3 v3.0.0-20200721060053-ca3cc9d940bc/go.mod h1:g1cyQ0CajooTxiCtJOcH+P5S2uC5cVwUhV3Rj4v0uUg= 46 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 52 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 53 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 54 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 55 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 57 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 58 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 59 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 60 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 61 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 62 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 63 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 64 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 65 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 66 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 67 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 68 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 69 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 70 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 74 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 76 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 77 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 78 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 79 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 83 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 84 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 85 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | -------------------------------------------------------------------------------- /examples/voice_receive/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | "github.com/pion/rtp" 10 | "github.com/pion/webrtc/v3/pkg/media" 11 | "github.com/pion/webrtc/v3/pkg/media/oggwriter" 12 | ) 13 | 14 | // Variables used for command line parameters 15 | var ( 16 | Token string 17 | ChannelID string 18 | GuildID string 19 | ) 20 | 21 | func init() { 22 | flag.StringVar(&Token, "t", "", "Bot Token") 23 | flag.StringVar(&GuildID, "g", "", "Guild in which voice channel exists") 24 | flag.StringVar(&ChannelID, "c", "", "Voice channel to connect to") 25 | flag.Parse() 26 | } 27 | 28 | func createPionRTPPacket(p *discordgo.Packet) *rtp.Packet { 29 | return &rtp.Packet{ 30 | Header: rtp.Header{ 31 | Version: 2, 32 | // Taken from Discord voice docs 33 | PayloadType: 0x78, 34 | SequenceNumber: p.Sequence, 35 | Timestamp: p.Timestamp, 36 | SSRC: p.SSRC, 37 | }, 38 | Payload: p.Opus, 39 | } 40 | } 41 | 42 | func handleVoice(c chan *discordgo.Packet) { 43 | files := make(map[uint32]media.Writer) 44 | for p := range c { 45 | file, ok := files[p.SSRC] 46 | if !ok { 47 | var err error 48 | file, err = oggwriter.New(fmt.Sprintf("%d.ogg", p.SSRC), 48000, 2) 49 | if err != nil { 50 | fmt.Printf("failed to create file %d.ogg, giving up on recording: %v\n", p.SSRC, err) 51 | return 52 | } 53 | files[p.SSRC] = file 54 | } 55 | // Construct pion RTP packet from DiscordGo's type. 56 | rtp := createPionRTPPacket(p) 57 | err := file.WriteRTP(rtp) 58 | if err != nil { 59 | fmt.Printf("failed to write to file %d.ogg, giving up on recording: %v\n", p.SSRC, err) 60 | } 61 | } 62 | 63 | // Once we made it here, we're done listening for packets. Close all files 64 | for _, f := range files { 65 | f.Close() 66 | } 67 | } 68 | 69 | func main() { 70 | s, err := discordgo.New("Bot " + Token) 71 | if err != nil { 72 | fmt.Println("error creating Discord session:", err) 73 | return 74 | } 75 | defer s.Close() 76 | 77 | // We only really care about receiving voice state updates. 78 | s.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsGuildVoiceStates) 79 | 80 | err = s.Open() 81 | if err != nil { 82 | fmt.Println("error opening connection:", err) 83 | return 84 | } 85 | 86 | v, err := s.ChannelVoiceJoin(GuildID, ChannelID, true, false) 87 | if err != nil { 88 | fmt.Println("failed to join voice channel:", err) 89 | return 90 | } 91 | 92 | go func() { 93 | time.Sleep(10 * time.Second) 94 | close(v.OpusRecv) 95 | v.Close() 96 | }() 97 | 98 | handleVoice(v.OpusRecv) 99 | } 100 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bwmarrin/discordgo 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.2 7 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 2 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 4 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 5 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 6 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 7 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 9 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 10 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 11 | -------------------------------------------------------------------------------- /interactions_test.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "encoding/hex" 7 | "net/http/httptest" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestVerifyInteraction(t *testing.T) { 15 | pubkey, privkey, err := ed25519.GenerateKey(nil) 16 | if err != nil { 17 | t.Errorf("error generating signing keypair: %s", err) 18 | } 19 | timestamp := "1608597133" 20 | 21 | t.Run("success", func(t *testing.T) { 22 | body := "body" 23 | request := httptest.NewRequest("POST", "http://localhost/interaction", strings.NewReader(body)) 24 | request.Header.Set("X-Signature-Timestamp", timestamp) 25 | 26 | var msg bytes.Buffer 27 | msg.WriteString(timestamp) 28 | msg.WriteString(body) 29 | signature := ed25519.Sign(privkey, msg.Bytes()) 30 | request.Header.Set("X-Signature-Ed25519", hex.EncodeToString(signature[:ed25519.SignatureSize])) 31 | 32 | if !VerifyInteraction(request, pubkey) { 33 | t.Error("expected true, got false") 34 | } 35 | }) 36 | 37 | t.Run("failure/modified body", func(t *testing.T) { 38 | body := "body" 39 | request := httptest.NewRequest("POST", "http://localhost/interaction", strings.NewReader("WRONG")) 40 | request.Header.Set("X-Signature-Timestamp", timestamp) 41 | 42 | var msg bytes.Buffer 43 | msg.WriteString(timestamp) 44 | msg.WriteString(body) 45 | signature := ed25519.Sign(privkey, msg.Bytes()) 46 | request.Header.Set("X-Signature-Ed25519", hex.EncodeToString(signature[:ed25519.SignatureSize])) 47 | 48 | if VerifyInteraction(request, pubkey) { 49 | t.Error("expected false, got true") 50 | } 51 | }) 52 | 53 | t.Run("failure/modified timestamp", func(t *testing.T) { 54 | body := "body" 55 | request := httptest.NewRequest("POST", "http://localhost/interaction", strings.NewReader("WRONG")) 56 | request.Header.Set("X-Signature-Timestamp", strconv.FormatInt(time.Now().Add(time.Minute).Unix(), 10)) 57 | 58 | var msg bytes.Buffer 59 | msg.WriteString(timestamp) 60 | msg.WriteString(body) 61 | signature := ed25519.Sign(privkey, msg.Bytes()) 62 | request.Header.Set("X-Signature-Ed25519", hex.EncodeToString(signature[:ed25519.SignatureSize])) 63 | 64 | if VerifyInteraction(request, pubkey) { 65 | t.Error("expected false, got true") 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /locales.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | // Locale represents the accepted languages for Discord. 4 | // https://discord.com/developers/docs/reference#locales 5 | type Locale string 6 | 7 | // String returns the human-readable string of the locale 8 | func (l Locale) String() string { 9 | if name, ok := Locales[l]; ok { 10 | return name 11 | } 12 | return Unknown.String() 13 | } 14 | 15 | // All defined locales in Discord 16 | const ( 17 | EnglishUS Locale = "en-US" 18 | EnglishGB Locale = "en-GB" 19 | Bulgarian Locale = "bg" 20 | ChineseCN Locale = "zh-CN" 21 | ChineseTW Locale = "zh-TW" 22 | Croatian Locale = "hr" 23 | Czech Locale = "cs" 24 | Danish Locale = "da" 25 | Dutch Locale = "nl" 26 | Finnish Locale = "fi" 27 | French Locale = "fr" 28 | German Locale = "de" 29 | Greek Locale = "el" 30 | Hindi Locale = "hi" 31 | Hungarian Locale = "hu" 32 | Italian Locale = "it" 33 | Japanese Locale = "ja" 34 | Korean Locale = "ko" 35 | Lithuanian Locale = "lt" 36 | Norwegian Locale = "no" 37 | Polish Locale = "pl" 38 | PortugueseBR Locale = "pt-BR" 39 | Romanian Locale = "ro" 40 | Russian Locale = "ru" 41 | SpanishES Locale = "es-ES" 42 | SpanishLATAM Locale = "es-419" 43 | Swedish Locale = "sv-SE" 44 | Thai Locale = "th" 45 | Turkish Locale = "tr" 46 | Ukrainian Locale = "uk" 47 | Vietnamese Locale = "vi" 48 | Unknown Locale = "" 49 | ) 50 | 51 | // Locales is a map of all the languages codes to their names. 52 | var Locales = map[Locale]string{ 53 | EnglishUS: "English (United States)", 54 | EnglishGB: "English (Great Britain)", 55 | Bulgarian: "Bulgarian", 56 | ChineseCN: "Chinese (China)", 57 | ChineseTW: "Chinese (Taiwan)", 58 | Croatian: "Croatian", 59 | Czech: "Czech", 60 | Danish: "Danish", 61 | Dutch: "Dutch", 62 | Finnish: "Finnish", 63 | French: "French", 64 | German: "German", 65 | Greek: "Greek", 66 | Hindi: "Hindi", 67 | Hungarian: "Hungarian", 68 | Italian: "Italian", 69 | Japanese: "Japanese", 70 | Korean: "Korean", 71 | Lithuanian: "Lithuanian", 72 | Norwegian: "Norwegian", 73 | Polish: "Polish", 74 | PortugueseBR: "Portuguese (Brazil)", 75 | Romanian: "Romanian", 76 | Russian: "Russian", 77 | SpanishES: "Spanish (Spain)", 78 | SpanishLATAM: "Spanish (LATAM)", 79 | Swedish: "Swedish", 80 | Thai: "Thai", 81 | Turkish: "Turkish", 82 | Ukrainian: "Ukrainian", 83 | Vietnamese: "Vietnamese", 84 | Unknown: "unknown", 85 | } 86 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | // Discordgo - Discord bindings for Go 2 | // Available at https://github.com/bwmarrin/discordgo 3 | 4 | // Copyright 2015-2016 Bruce Marriner . All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | // This file contains code related to discordgo package logging 9 | 10 | package discordgo 11 | 12 | import ( 13 | "fmt" 14 | "log" 15 | "runtime" 16 | "strings" 17 | ) 18 | 19 | const ( 20 | 21 | // LogError level is used for critical errors that could lead to data loss 22 | // or panic that would not be returned to a calling function. 23 | LogError int = iota 24 | 25 | // LogWarning level is used for very abnormal events and errors that are 26 | // also returned to a calling function. 27 | LogWarning 28 | 29 | // LogInformational level is used for normal non-error activity 30 | LogInformational 31 | 32 | // LogDebug level is for very detailed non-error activity. This is 33 | // very spammy and will impact performance. 34 | LogDebug 35 | ) 36 | 37 | // Logger can be used to replace the standard logging for discordgo 38 | var Logger func(msgL, caller int, format string, a ...interface{}) 39 | 40 | // msglog provides package wide logging consistency for discordgo 41 | // the format, a... portion this command follows that of fmt.Printf 42 | // msgL : LogLevel of the message 43 | // caller : 1 + the number of callers away from the message source 44 | // format : Printf style message format 45 | // a ... : comma separated list of values to pass 46 | func msglog(msgL, caller int, format string, a ...interface{}) { 47 | 48 | if Logger != nil { 49 | Logger(msgL, caller, format, a...) 50 | } else { 51 | 52 | pc, file, line, _ := runtime.Caller(caller) 53 | 54 | files := strings.Split(file, "/") 55 | file = files[len(files)-1] 56 | 57 | name := runtime.FuncForPC(pc).Name() 58 | fns := strings.Split(name, ".") 59 | name = fns[len(fns)-1] 60 | 61 | msg := fmt.Sprintf(format, a...) 62 | 63 | log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg) 64 | } 65 | } 66 | 67 | // helper function that wraps msglog for the Session struct 68 | // This adds a check to insure the message is only logged 69 | // if the session log level is equal or higher than the 70 | // message log level 71 | func (s *Session) log(msgL int, format string, a ...interface{}) { 72 | 73 | if msgL > s.LogLevel { 74 | return 75 | } 76 | 77 | msglog(msgL, 2, format, a...) 78 | } 79 | 80 | // helper function that wraps msglog for the VoiceConnection struct 81 | // This adds a check to insure the message is only logged 82 | // if the voice connection log level is equal or higher than the 83 | // message log level 84 | func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { 85 | 86 | if msgL > v.LogLevel { 87 | return 88 | } 89 | 90 | msglog(msgL, 2, format, a...) 91 | } 92 | 93 | // printJSON is a helper function to display JSON data in an easy to read format. 94 | /* NOT USED ATM 95 | func printJSON(body []byte) { 96 | var prettyJSON bytes.Buffer 97 | error := json.Indent(&prettyJSON, body, "", "\t") 98 | if error != nil { 99 | log.Print("JSON parse error: ", error) 100 | } 101 | log.Println(string(prettyJSON.Bytes())) 102 | } 103 | */ 104 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestContentWithMoreMentionsReplaced(t *testing.T) { 8 | s := &Session{StateEnabled: true, State: NewState()} 9 | 10 | user := &User{ 11 | ID: "user", 12 | Username: "User Name", 13 | } 14 | 15 | s.State.GuildAdd(&Guild{ID: "guild"}) 16 | s.State.RoleAdd("guild", &Role{ 17 | ID: "role", 18 | Name: "Role Name", 19 | Mentionable: true, 20 | }) 21 | s.State.MemberAdd(&Member{ 22 | User: user, 23 | Nick: "User Nick", 24 | GuildID: "guild", 25 | }) 26 | s.State.ChannelAdd(&Channel{ 27 | Name: "Channel Name", 28 | GuildID: "guild", 29 | ID: "channel", 30 | }) 31 | m := &Message{ 32 | Content: "<@&role> <@!user> <@user> <#channel>", 33 | ChannelID: "channel", 34 | MentionRoles: []string{"role"}, 35 | Mentions: []*User{user}, 36 | } 37 | if result, _ := m.ContentWithMoreMentionsReplaced(s); result != "@Role Name @User Nick @User Name #Channel Name" { 38 | t.Error(result) 39 | } 40 | } 41 | func TestGettingEmojisFromMessage(t *testing.T) { 42 | msg := "test test <:kitty14:811736565172011058> <:kitty4:811736468812595260>" 43 | m := &Message{ 44 | Content: msg, 45 | } 46 | emojis := m.GetCustomEmojis() 47 | if len(emojis) < 1 { 48 | t.Error("No emojis found.") 49 | return 50 | } 51 | 52 | } 53 | 54 | func TestMessage_Reference(t *testing.T) { 55 | m := &Message{ 56 | ID: "811736565172011001", 57 | GuildID: "811736565172011002", 58 | ChannelID: "811736565172011003", 59 | } 60 | 61 | ref := m.Reference() 62 | 63 | if ref.Type != 0 { 64 | t.Error("Default reference type should be 0") 65 | } 66 | 67 | if ref.MessageID != m.ID { 68 | t.Error("Message ID should be the same") 69 | } 70 | 71 | if ref.GuildID != m.GuildID { 72 | t.Error("Guild ID should be the same") 73 | } 74 | 75 | if ref.ChannelID != m.ChannelID { 76 | t.Error("Channel ID should be the same") 77 | } 78 | } 79 | 80 | func TestMessage_Forward(t *testing.T) { 81 | m := &Message{ 82 | ID: "811736565172011001", 83 | GuildID: "811736565172011002", 84 | ChannelID: "811736565172011003", 85 | } 86 | 87 | ref := m.Forward() 88 | 89 | if ref.Type != MessageReferenceTypeForward { 90 | t.Error("Reference type should be 1 (forward)") 91 | } 92 | 93 | if ref.MessageID != m.ID { 94 | t.Error("Message ID should be the same") 95 | } 96 | 97 | if ref.GuildID != m.GuildID { 98 | t.Error("Guild ID should be the same") 99 | } 100 | 101 | if ref.ChannelID != m.ChannelID { 102 | t.Error("Channel ID should be the same") 103 | } 104 | } 105 | 106 | func TestMessageReference_DefaultTypeIsDefault(t *testing.T) { 107 | r := MessageReference{} 108 | if r.Type != MessageReferenceTypeDefault { 109 | t.Error("Default message type should be MessageReferenceTypeDefault") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: DiscordGo 2 | site_author: Bruce Marriner 3 | site_url: http://bwmarrin.github.io/discordgo/ 4 | repo_url: https://github.com/bwmarrin/discordgo 5 | 6 | dev_addr: 0.0.0.0:8000 7 | theme: yeti 8 | 9 | markdown_extensions: 10 | - smarty 11 | - toc: 12 | permalink: True 13 | - sane_lists 14 | 15 | pages: 16 | - 'Home': 'index.md' 17 | - 'Getting Started': 'GettingStarted.md' 18 | -------------------------------------------------------------------------------- /oauth2.go: -------------------------------------------------------------------------------- 1 | // Discordgo - Discord bindings for Go 2 | // Available at https://github.com/bwmarrin/discordgo 3 | 4 | // Copyright 2015-2016 Bruce Marriner . All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | // This file contains functions related to Discord OAuth2 endpoints 9 | 10 | package discordgo 11 | 12 | // ------------------------------------------------------------------------------------------------ 13 | // Code specific to Discord OAuth2 Applications 14 | // ------------------------------------------------------------------------------------------------ 15 | 16 | // The MembershipState represents whether the user is in the team or has been invited into it 17 | type MembershipState int 18 | 19 | // Constants for the different stages of the MembershipState 20 | const ( 21 | MembershipStateInvited MembershipState = 1 22 | MembershipStateAccepted MembershipState = 2 23 | ) 24 | 25 | // A TeamMember struct stores values for a single Team Member, extending the normal User data - note that the user field is partial 26 | type TeamMember struct { 27 | User *User `json:"user"` 28 | TeamID string `json:"team_id"` 29 | MembershipState MembershipState `json:"membership_state"` 30 | Permissions []string `json:"permissions"` 31 | } 32 | 33 | // A Team struct stores the members of a Discord Developer Team as well as some metadata about it 34 | type Team struct { 35 | ID string `json:"id"` 36 | Name string `json:"name"` 37 | Description string `json:"description"` 38 | Icon string `json:"icon"` 39 | OwnerID string `json:"owner_user_id"` 40 | Members []*TeamMember `json:"members"` 41 | } 42 | 43 | // Application returns an Application structure of a specific Application 44 | // appID : The ID of an Application 45 | func (s *Session) Application(appID string) (st *Application, err error) { 46 | 47 | body, err := s.RequestWithBucketID("GET", EndpointOAuth2Application(appID), nil, EndpointOAuth2Application("")) 48 | if err != nil { 49 | return 50 | } 51 | 52 | err = unmarshal(body, &st) 53 | return 54 | } 55 | 56 | // Applications returns all applications for the authenticated user 57 | func (s *Session) Applications() (st []*Application, err error) { 58 | 59 | body, err := s.RequestWithBucketID("GET", EndpointOAuth2Applications, nil, EndpointOAuth2Applications) 60 | if err != nil { 61 | return 62 | } 63 | 64 | err = unmarshal(body, &st) 65 | return 66 | } 67 | 68 | // ApplicationCreate creates a new Application 69 | // name : Name of Application / Bot 70 | // uris : Redirect URIs (Not required) 71 | func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error) { 72 | 73 | data := struct { 74 | Name string `json:"name"` 75 | Description string `json:"description"` 76 | }{ap.Name, ap.Description} 77 | 78 | body, err := s.RequestWithBucketID("POST", EndpointOAuth2Applications, data, EndpointOAuth2Applications) 79 | if err != nil { 80 | return 81 | } 82 | 83 | err = unmarshal(body, &st) 84 | return 85 | } 86 | 87 | // ApplicationUpdate updates an existing Application 88 | // var : desc 89 | func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Application, err error) { 90 | 91 | data := struct { 92 | Name string `json:"name"` 93 | Description string `json:"description"` 94 | }{ap.Name, ap.Description} 95 | 96 | body, err := s.RequestWithBucketID("PUT", EndpointOAuth2Application(appID), data, EndpointOAuth2Application("")) 97 | if err != nil { 98 | return 99 | } 100 | 101 | err = unmarshal(body, &st) 102 | return 103 | } 104 | 105 | // ApplicationDelete deletes an existing Application 106 | // appID : The ID of an Application 107 | func (s *Session) ApplicationDelete(appID string) (err error) { 108 | 109 | _, err = s.RequestWithBucketID("DELETE", EndpointOAuth2Application(appID), nil, EndpointOAuth2Application("")) 110 | if err != nil { 111 | return 112 | } 113 | 114 | return 115 | } 116 | 117 | // Asset struct stores values for an asset of an application 118 | type Asset struct { 119 | Type int `json:"type"` 120 | ID string `json:"id"` 121 | Name string `json:"name"` 122 | } 123 | 124 | // ApplicationAssets returns an application's assets 125 | func (s *Session) ApplicationAssets(appID string) (ass []*Asset, err error) { 126 | 127 | body, err := s.RequestWithBucketID("GET", EndpointOAuth2ApplicationAssets(appID), nil, EndpointOAuth2ApplicationAssets("")) 128 | if err != nil { 129 | return 130 | } 131 | 132 | err = unmarshal(body, &ass) 133 | return 134 | } 135 | 136 | // ------------------------------------------------------------------------------------------------ 137 | // Code specific to Discord OAuth2 Application Bots 138 | // ------------------------------------------------------------------------------------------------ 139 | 140 | // ApplicationBotCreate creates an Application Bot Account 141 | // 142 | // appID : The ID of an Application 143 | // 144 | // NOTE: func name may change, if I can think up something better. 145 | func (s *Session) ApplicationBotCreate(appID string) (st *User, err error) { 146 | 147 | body, err := s.RequestWithBucketID("POST", EndpointOAuth2ApplicationsBot(appID), nil, EndpointOAuth2ApplicationsBot("")) 148 | if err != nil { 149 | return 150 | } 151 | 152 | err = unmarshal(body, &st) 153 | return 154 | } 155 | -------------------------------------------------------------------------------- /oauth2_test.go: -------------------------------------------------------------------------------- 1 | package discordgo_test 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | func ExampleApplication() { 11 | 12 | // Authentication Token pulled from environment variable DGU_TOKEN 13 | Token := os.Getenv("DGU_TOKEN") 14 | if Token == "" { 15 | return 16 | } 17 | 18 | // Create a new Discordgo session 19 | dg, err := discordgo.New(Token) 20 | if err != nil { 21 | log.Println(err) 22 | return 23 | } 24 | 25 | // Create an new Application 26 | ap := &discordgo.Application{} 27 | ap.Name = "TestApp" 28 | ap.Description = "TestDesc" 29 | ap, err = dg.ApplicationCreate(ap) 30 | log.Printf("ApplicationCreate: err: %+v, app: %+v\n", err, ap) 31 | 32 | // Get a specific Application by it's ID 33 | ap, err = dg.Application(ap.ID) 34 | log.Printf("Application: err: %+v, app: %+v\n", err, ap) 35 | 36 | // Update an existing Application with new values 37 | ap.Description = "Whooooa" 38 | ap, err = dg.ApplicationUpdate(ap.ID, ap) 39 | log.Printf("ApplicationUpdate: err: %+v, app: %+v\n", err, ap) 40 | 41 | // create a new bot account for this application 42 | bot, err := dg.ApplicationBotCreate(ap.ID) 43 | log.Printf("BotCreate: err: %+v, bot: %+v\n", err, bot) 44 | 45 | // Get a list of all applications for the authenticated user 46 | apps, err := dg.Applications() 47 | log.Printf("Applications: err: %+v, apps : %+v\n", err, apps) 48 | for k, v := range apps { 49 | log.Printf("Applications: %d : %+v\n", k, v) 50 | } 51 | 52 | // Delete the application we created. 53 | err = dg.ApplicationDelete(ap.ID) 54 | log.Printf("Delete: err: %+v\n", err) 55 | 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /ratelimit.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "math" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | // customRateLimit holds information for defining a custom rate limit 14 | type customRateLimit struct { 15 | suffix string 16 | requests int 17 | reset time.Duration 18 | } 19 | 20 | // RateLimiter holds all ratelimit buckets 21 | type RateLimiter struct { 22 | sync.Mutex 23 | global *int64 24 | buckets map[string]*Bucket 25 | globalRateLimit time.Duration 26 | customRateLimits []*customRateLimit 27 | } 28 | 29 | // NewRatelimiter returns a new RateLimiter 30 | func NewRatelimiter() *RateLimiter { 31 | 32 | return &RateLimiter{ 33 | buckets: make(map[string]*Bucket), 34 | global: new(int64), 35 | customRateLimits: []*customRateLimit{ 36 | { 37 | suffix: "//reactions//", 38 | requests: 1, 39 | reset: 200 * time.Millisecond, 40 | }, 41 | }, 42 | } 43 | } 44 | 45 | // GetBucket retrieves or creates a bucket 46 | func (r *RateLimiter) GetBucket(key string) *Bucket { 47 | r.Lock() 48 | defer r.Unlock() 49 | 50 | if bucket, ok := r.buckets[key]; ok { 51 | return bucket 52 | } 53 | 54 | b := &Bucket{ 55 | Remaining: 1, 56 | Key: key, 57 | global: r.global, 58 | } 59 | 60 | // Check if there is a custom ratelimit set for this bucket ID. 61 | for _, rl := range r.customRateLimits { 62 | if strings.HasSuffix(b.Key, rl.suffix) { 63 | b.customRateLimit = rl 64 | break 65 | } 66 | } 67 | 68 | r.buckets[key] = b 69 | return b 70 | } 71 | 72 | // GetWaitTime returns the duration you should wait for a Bucket 73 | func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration { 74 | // If we ran out of calls and the reset time is still ahead of us 75 | // then we need to take it easy and relax a little 76 | if b.Remaining < minRemaining && b.reset.After(time.Now()) { 77 | return b.reset.Sub(time.Now()) 78 | } 79 | 80 | // Check for global ratelimits 81 | sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) 82 | if now := time.Now(); now.Before(sleepTo) { 83 | return sleepTo.Sub(now) 84 | } 85 | 86 | return 0 87 | } 88 | 89 | // LockBucket Locks until a request can be made 90 | func (r *RateLimiter) LockBucket(bucketID string) *Bucket { 91 | return r.LockBucketObject(r.GetBucket(bucketID)) 92 | } 93 | 94 | // LockBucketObject Locks an already resolved bucket until a request can be made 95 | func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket { 96 | b.Lock() 97 | 98 | if wait := r.GetWaitTime(b, 1); wait > 0 { 99 | time.Sleep(wait) 100 | } 101 | 102 | b.Remaining-- 103 | return b 104 | } 105 | 106 | // Bucket represents a ratelimit bucket, each bucket gets ratelimited individually (-global ratelimits) 107 | type Bucket struct { 108 | sync.Mutex 109 | Key string 110 | Remaining int 111 | limit int 112 | reset time.Time 113 | global *int64 114 | 115 | lastReset time.Time 116 | customRateLimit *customRateLimit 117 | Userdata interface{} 118 | } 119 | 120 | // Release unlocks the bucket and reads the headers to update the buckets ratelimit info 121 | // and locks up the whole thing in case if there's a global ratelimit. 122 | func (b *Bucket) Release(headers http.Header) error { 123 | defer b.Unlock() 124 | 125 | // Check if the bucket uses a custom ratelimiter 126 | if rl := b.customRateLimit; rl != nil { 127 | if time.Now().Sub(b.lastReset) >= rl.reset { 128 | b.Remaining = rl.requests - 1 129 | b.lastReset = time.Now() 130 | } 131 | if b.Remaining < 1 { 132 | b.reset = time.Now().Add(rl.reset) 133 | } 134 | return nil 135 | } 136 | 137 | if headers == nil { 138 | return nil 139 | } 140 | 141 | remaining := headers.Get("X-RateLimit-Remaining") 142 | reset := headers.Get("X-RateLimit-Reset") 143 | global := headers.Get("X-RateLimit-Global") 144 | resetAfter := headers.Get("X-RateLimit-Reset-After") 145 | 146 | // Update global and per bucket reset time if the proper headers are available 147 | // If global is set, then it will block all buckets until after Retry-After 148 | // If Retry-After without global is provided it will use that for the new reset 149 | // time since it's more accurate than X-RateLimit-Reset. 150 | // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset 151 | if resetAfter != "" { 152 | parsedAfter, err := strconv.ParseFloat(resetAfter, 64) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | whole, frac := math.Modf(parsedAfter) 158 | resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * time.Millisecond) 159 | 160 | // Lock either this single bucket or all buckets 161 | if global != "" { 162 | atomic.StoreInt64(b.global, resetAt.UnixNano()) 163 | } else { 164 | b.reset = resetAt 165 | } 166 | } else if reset != "" { 167 | // Calculate the reset time by using the date header returned from discord 168 | discordTime, err := http.ParseTime(headers.Get("Date")) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | unix, err := strconv.ParseFloat(reset, 64) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | // Calculate the time until reset and add it to the current local time 179 | // some extra time is added because without it i still encountered 429's. 180 | // The added amount is the lowest amount that gave no 429's 181 | // in 1k requests 182 | whole, frac := math.Modf(unix) 183 | delta := time.Unix(int64(whole), 0).Add(time.Duration(frac*1000)*time.Millisecond).Sub(discordTime) + time.Millisecond*250 184 | b.reset = time.Now().Add(delta) 185 | } 186 | 187 | // Udpate remaining if header is present 188 | if remaining != "" { 189 | parsedRemaining, err := strconv.ParseInt(remaining, 10, 32) 190 | if err != nil { 191 | return err 192 | } 193 | b.Remaining = int(parsedRemaining) 194 | } 195 | 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /ratelimit_test.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // This test takes ~2 seconds to run 12 | func TestRatelimitReset(t *testing.T) { 13 | rl := NewRatelimiter() 14 | 15 | sendReq := func(endpoint string) { 16 | bucket := rl.LockBucket(endpoint) 17 | 18 | headers := http.Header(make(map[string][]string)) 19 | 20 | headers.Set("X-RateLimit-Remaining", "0") 21 | // Reset for approx 2 seconds from now 22 | headers.Set("X-RateLimit-Reset", fmt.Sprint(float64(time.Now().Add(time.Second*2).UnixNano())/1e9)) 23 | headers.Set("Date", time.Now().Format(time.RFC850)) 24 | 25 | err := bucket.Release(headers) 26 | if err != nil { 27 | t.Errorf("Release returned error: %v", err) 28 | } 29 | } 30 | 31 | sent := time.Now() 32 | sendReq("/guilds/99/channels") 33 | sendReq("/guilds/55/channels") 34 | sendReq("/guilds/66/channels") 35 | 36 | sendReq("/guilds/99/channels") 37 | sendReq("/guilds/55/channels") 38 | sendReq("/guilds/66/channels") 39 | 40 | // We hit the same endpoint 2 times, so we should only be ratelimited 2 second 41 | // And always less than 4 seconds (unless you're on a stoneage computer or using swap or something...) 42 | if time.Since(sent) >= time.Second && time.Since(sent) < time.Second*4 { 43 | t.Log("OK", time.Since(sent)) 44 | } else { 45 | t.Error("Did not ratelimit correctly, got:", time.Since(sent)) 46 | } 47 | } 48 | 49 | // This test takes ~1 seconds to run 50 | func TestRatelimitGlobal(t *testing.T) { 51 | rl := NewRatelimiter() 52 | 53 | sendReq := func(endpoint string) { 54 | bucket := rl.LockBucket(endpoint) 55 | 56 | headers := http.Header(make(map[string][]string)) 57 | 58 | headers.Set("X-RateLimit-Global", "1") 59 | // Reset for approx 1 seconds from now 60 | headers.Set("X-RateLimit-Reset-After", "1") 61 | 62 | err := bucket.Release(headers) 63 | if err != nil { 64 | t.Errorf("Release returned error: %v", err) 65 | } 66 | } 67 | 68 | sent := time.Now() 69 | 70 | // This should trigger a global ratelimit 71 | sendReq("/guilds/99/channels") 72 | time.Sleep(time.Millisecond * 100) 73 | 74 | // This shouldn't go through in less than 1 second 75 | sendReq("/guilds/55/channels") 76 | 77 | if time.Since(sent) >= time.Second && time.Since(sent) < time.Second*2 { 78 | t.Log("OK", time.Since(sent)) 79 | } else { 80 | t.Error("Did not ratelimit correctly, got:", time.Since(sent)) 81 | } 82 | } 83 | 84 | func BenchmarkRatelimitSingleEndpoint(b *testing.B) { 85 | rl := NewRatelimiter() 86 | for i := 0; i < b.N; i++ { 87 | sendBenchReq("/guilds/99/channels", rl) 88 | } 89 | } 90 | 91 | func BenchmarkRatelimitParallelMultiEndpoints(b *testing.B) { 92 | rl := NewRatelimiter() 93 | b.RunParallel(func(pb *testing.PB) { 94 | i := 0 95 | for pb.Next() { 96 | sendBenchReq("/guilds/"+strconv.Itoa(i)+"/channels", rl) 97 | i++ 98 | } 99 | }) 100 | } 101 | 102 | // Does not actually send requests, but locks the bucket and releases it with made-up headers 103 | func sendBenchReq(endpoint string, rl *RateLimiter) { 104 | bucket := rl.LockBucket(endpoint) 105 | 106 | headers := http.Header(make(map[string][]string)) 107 | 108 | headers.Set("X-RateLimit-Remaining", "10") 109 | headers.Set("X-RateLimit-Reset", fmt.Sprint(float64(time.Now().UnixNano())/1e9)) 110 | headers.Set("Date", time.Now().Format(time.RFC850)) 111 | 112 | bucket.Release(headers) 113 | } 114 | -------------------------------------------------------------------------------- /restapi_test.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "testing" 8 | ) 9 | 10 | ////////////////////////////////////////////////////////////////////////////// 11 | /////////////////////////////////////////////////////////////// START OF TESTS 12 | 13 | // TestChannelMessageSend tests the ChannelMessageSend() function. This should not return an error. 14 | func TestChannelMessageSend(t *testing.T) { 15 | 16 | if envChannel == "" { 17 | t.Skip("Skipping, DG_CHANNEL not set.") 18 | } 19 | 20 | if dg == nil { 21 | t.Skip("Skipping, dg not set.") 22 | } 23 | 24 | _, err := dg.ChannelMessageSend(envChannel, "Running REST API Tests!") 25 | if err != nil { 26 | t.Errorf("ChannelMessageSend returned error: %+v", err) 27 | } 28 | } 29 | 30 | /* 31 | // removed for now, only works on BOT accounts now 32 | func TestUserAvatar(t *testing.T) { 33 | 34 | if dg == nil { 35 | t.Skip("Cannot TestUserAvatar, dg not set.") 36 | } 37 | 38 | u, err := dg.User("@me") 39 | if err != nil { 40 | t.Error("error fetching @me user,", err) 41 | } 42 | 43 | a, err := dg.UserAvatar(u.ID) 44 | if err != nil { 45 | if err.Error() == `HTTP 404 NOT FOUND, {"code": 0, "message": "404: Not Found"}` { 46 | t.Skip("Skipped, @me doesn't have an Avatar") 47 | } 48 | t.Errorf(err.Error()) 49 | } 50 | 51 | if a == nil { 52 | t.Errorf("a == nil, should be image.Image") 53 | } 54 | } 55 | */ 56 | 57 | /* Running this causes an error due to 2/hour rate limit on username changes 58 | func TestUserUpdate(t *testing.T) { 59 | if dg == nil { 60 | t.Skip("Cannot test logout, dg not set.") 61 | } 62 | 63 | u, err := dg.User("@me") 64 | if err != nil { 65 | t.Errorf(err.Error()) 66 | } 67 | 68 | s, err := dg.UserUpdate(envEmail, envPassword, "testname", u.Avatar, "") 69 | if err != nil { 70 | t.Error(err.Error()) 71 | } 72 | if s.Username != "testname" { 73 | t.Error("Username != testname") 74 | } 75 | s, err = dg.UserUpdate(envEmail, envPassword, u.Username, u.Avatar, "") 76 | if err != nil { 77 | t.Error(err.Error()) 78 | } 79 | if s.Username != u.Username { 80 | t.Error("Username != " + u.Username) 81 | } 82 | } 83 | */ 84 | 85 | //func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) { 86 | 87 | func TestUserChannelCreate(t *testing.T) { 88 | if dg == nil { 89 | t.Skip("Cannot TestUserChannelCreate, dg not set.") 90 | } 91 | 92 | if envAdmin == "" { 93 | t.Skip("Skipped, DG_ADMIN not set.") 94 | } 95 | 96 | _, err := dg.UserChannelCreate(envAdmin) 97 | if err != nil { 98 | t.Errorf(err.Error()) 99 | } 100 | 101 | // TODO make sure the channel was added 102 | } 103 | 104 | func TestUserGuilds(t *testing.T) { 105 | if dg == nil { 106 | t.Skip("Cannot TestUserGuilds, dg not set.") 107 | } 108 | 109 | _, err := dg.UserGuilds(10, "", "", false) 110 | if err != nil { 111 | t.Errorf(err.Error()) 112 | } 113 | } 114 | 115 | func TestGateway(t *testing.T) { 116 | 117 | if dg == nil { 118 | t.Skip("Skipping, dg not set.") 119 | } 120 | _, err := dg.Gateway() 121 | if err != nil { 122 | t.Errorf("Gateway() returned error: %+v", err) 123 | } 124 | } 125 | 126 | func TestGatewayBot(t *testing.T) { 127 | 128 | if dgBot == nil { 129 | t.Skip("Skipping, dgBot not set.") 130 | } 131 | _, err := dgBot.GatewayBot() 132 | if err != nil { 133 | t.Errorf("GatewayBot() returned error: %+v", err) 134 | } 135 | } 136 | 137 | func TestVoiceRegions(t *testing.T) { 138 | 139 | if dg == nil { 140 | t.Skip("Skipping, dg not set.") 141 | } 142 | 143 | _, err := dg.VoiceRegions() 144 | if err != nil { 145 | t.Errorf("VoiceRegions() returned error: %+v", err) 146 | } 147 | } 148 | func TestGuildRoles(t *testing.T) { 149 | 150 | if envGuild == "" { 151 | t.Skip("Skipping, DG_GUILD not set.") 152 | } 153 | 154 | if dg == nil { 155 | t.Skip("Skipping, dg not set.") 156 | } 157 | 158 | _, err := dg.GuildRoles(envGuild) 159 | if err != nil { 160 | t.Errorf("GuildRoles(envGuild) returned error: %+v", err) 161 | } 162 | 163 | } 164 | 165 | func TestGuildMemberNickname(t *testing.T) { 166 | 167 | if envGuild == "" { 168 | t.Skip("Skipping, DG_GUILD not set.") 169 | } 170 | 171 | if dg == nil { 172 | t.Skip("Skipping, dg not set.") 173 | } 174 | 175 | err := dg.GuildMemberNickname(envGuild, "@me/nick", "B1nzyRocks") 176 | if err != nil { 177 | t.Errorf("GuildNickname returned error: %+v", err) 178 | } 179 | } 180 | 181 | // TestChannelMessageSend2 tests the ChannelMessageSend() function. This should not return an error. 182 | func TestChannelMessageSend2(t *testing.T) { 183 | 184 | if envChannel == "" { 185 | t.Skip("Skipping, DG_CHANNEL not set.") 186 | } 187 | 188 | if dg == nil { 189 | t.Skip("Skipping, dg not set.") 190 | } 191 | 192 | _, err := dg.ChannelMessageSend(envChannel, "All done running REST API Tests!") 193 | if err != nil { 194 | t.Errorf("ChannelMessageSend returned error: %+v", err) 195 | } 196 | } 197 | 198 | // TestGuildPruneCount tests GuildPruneCount() function. This should not return an error. 199 | func TestGuildPruneCount(t *testing.T) { 200 | 201 | if envGuild == "" { 202 | t.Skip("Skipping, DG_GUILD not set.") 203 | } 204 | 205 | if dg == nil { 206 | t.Skip("Skipping, dg not set.") 207 | } 208 | 209 | _, err := dg.GuildPruneCount(envGuild, 1) 210 | if err != nil { 211 | t.Errorf("GuildPruneCount returned error: %+v", err) 212 | } 213 | } 214 | 215 | /* 216 | // TestGuildPrune tests GuildPrune() function. This should not return an error. 217 | func TestGuildPrune(t *testing.T) { 218 | 219 | if envGuild == "" { 220 | t.Skip("Skipping, DG_GUILD not set.") 221 | } 222 | 223 | if dg == nil { 224 | t.Skip("Skipping, dg not set.") 225 | } 226 | 227 | _, err := dg.GuildPrune(envGuild, 1) 228 | if err != nil { 229 | t.Errorf("GuildPrune returned error: %+v", err) 230 | } 231 | } 232 | */ 233 | 234 | func Test_unmarshal(t *testing.T) { 235 | err := unmarshal([]byte{}, &struct{}{}) 236 | if !errors.Is(err, ErrJSONUnmarshal) { 237 | t.Errorf("Unexpected error type: %T", err) 238 | } 239 | } 240 | 241 | func TestWithContext(t *testing.T) { 242 | // Set up a test context. 243 | type key struct{} 244 | ctx := context.WithValue(context.Background(), key{}, "value") 245 | 246 | // Set up a test client. 247 | session, err := New("") 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | 252 | testErr := errors.New("test") 253 | 254 | // Intercept the request to assert the context. 255 | session.Client.Transport = roundTripperFunc(func(r *http.Request) (*http.Response, error) { 256 | val, _ := r.Context().Value(key{}).(string) 257 | if val != "value" { 258 | t.Errorf("missing value in context (got %q, wanted %q)", val, "value") 259 | } 260 | return nil, testErr 261 | }) 262 | 263 | // Run any client method using WithContext. 264 | _, err = session.User("", WithContext(ctx)) 265 | 266 | // Verify that the assertion code was actually run. 267 | if !errors.Is(err, testErr) { 268 | t.Errorf("unexpected error %v returned from client", err) 269 | } 270 | } 271 | 272 | // roundTripperFunc implements http.RoundTripper. 273 | type roundTripperFunc func(*http.Request) (*http.Response, error) 274 | 275 | func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { 276 | return f(req) 277 | } 278 | -------------------------------------------------------------------------------- /structs_test.go: -------------------------------------------------------------------------------- 1 | // Discordgo - Discord bindings for Go 2 | // Available at https://github.com/bwmarrin/discordgo 3 | 4 | // Copyright 2015-2016 Bruce Marriner . All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | package discordgo 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestMember_DisplayName(t *testing.T) { 15 | user := &User{ 16 | GlobalName: "Global", 17 | } 18 | t.Run("no server nickname set", func(t *testing.T) { 19 | m := &Member{ 20 | Nick: "", 21 | User: user, 22 | } 23 | want := user.DisplayName() 24 | if dn := m.DisplayName(); dn != want { 25 | t.Errorf("Member.DisplayName() = %v, want %v", dn, want) 26 | } 27 | }) 28 | t.Run("server nickname set", func(t *testing.T) { 29 | m := &Member{ 30 | Nick: "Server", 31 | User: user, 32 | } 33 | if dn := m.DisplayName(); dn != m.Nick { 34 | t.Errorf("Member.DisplayName() = %v, want %v", dn, m.Nick) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /tools/cmd/eventhandlers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "go/parser" 7 | "go/token" 8 | "io/ioutil" 9 | "log" 10 | "path/filepath" 11 | "regexp" 12 | "sort" 13 | "strings" 14 | "text/template" 15 | ) 16 | 17 | var eventHandlerTmpl = template.Must(template.New("eventHandler").Funcs(template.FuncMap{ 18 | "constName": constName, 19 | "isDiscordEvent": isDiscordEvent, 20 | "privateName": privateName, 21 | }).Parse(`// Code generated by \"eventhandlers\"; DO NOT EDIT 22 | // See events.go 23 | 24 | package discordgo 25 | 26 | // Following are all the event types. 27 | // Event type values are used to match the events returned by Discord. 28 | // EventTypes surrounded by __ are synthetic and are internal to DiscordGo. 29 | const ({{range .}} 30 | {{privateName .}}EventType = "{{constName .}}"{{end}} 31 | ) 32 | {{range .}} 33 | // {{privateName .}}EventHandler is an event handler for {{.}} events. 34 | type {{privateName .}}EventHandler func(*Session, *{{.}}) 35 | 36 | // Type returns the event type for {{.}} events. 37 | func (eh {{privateName .}}EventHandler) Type() string { 38 | return {{privateName .}}EventType 39 | } 40 | {{if isDiscordEvent .}} 41 | // New returns a new instance of {{.}}. 42 | func (eh {{privateName .}}EventHandler) New() interface{} { 43 | return &{{.}}{} 44 | }{{end}} 45 | // Handle is the handler for {{.}} events. 46 | func (eh {{privateName .}}EventHandler) Handle(s *Session, i interface{}) { 47 | if t, ok := i.(*{{.}}); ok { 48 | eh(s, t) 49 | } 50 | } 51 | 52 | {{end}} 53 | func handlerForInterface(handler interface{}) EventHandler { 54 | switch v := handler.(type) { 55 | case func(*Session, interface{}): 56 | return interfaceEventHandler(v){{range .}} 57 | case func(*Session, *{{.}}): 58 | return {{privateName .}}EventHandler(v){{end}} 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func init() { {{range .}}{{if isDiscordEvent .}} 65 | registerInterfaceProvider({{privateName .}}EventHandler(nil)){{end}}{{end}} 66 | } 67 | `)) 68 | 69 | func main() { 70 | var buf bytes.Buffer 71 | dir := filepath.Dir(".") 72 | 73 | fs := token.NewFileSet() 74 | parsedFile, err := parser.ParseFile(fs, "events.go", nil, 0) 75 | if err != nil { 76 | log.Fatalf("warning: internal error: could not parse events.go: %s", err) 77 | return 78 | } 79 | 80 | names := []string{} 81 | for object := range parsedFile.Scope.Objects { 82 | names = append(names, object) 83 | } 84 | sort.Strings(names) 85 | eventHandlerTmpl.Execute(&buf, names) 86 | 87 | src, err := format.Source(buf.Bytes()) 88 | if err != nil { 89 | log.Println("warning: internal error: invalid Go generated:", err) 90 | src = buf.Bytes() 91 | } 92 | 93 | err = ioutil.WriteFile(filepath.Join(dir, strings.ToLower("eventhandlers.go")), src, 0644) 94 | if err != nil { 95 | log.Fatal(buf, "writing output: %s", err) 96 | } 97 | } 98 | 99 | var constRegexp = regexp.MustCompile("([a-z])([A-Z])") 100 | 101 | func constCase(name string) string { 102 | return strings.ToUpper(constRegexp.ReplaceAllString(name, "${1}_${2}")) 103 | } 104 | 105 | func isDiscordEvent(name string) bool { 106 | switch { 107 | case name == "Connect", name == "Disconnect", name == "Event", name == "RateLimit", name == "Interface": 108 | return false 109 | default: 110 | return true 111 | } 112 | } 113 | 114 | func constName(name string) string { 115 | if !isDiscordEvent(name) { 116 | return "__" + constCase(name) + "__" 117 | } 118 | 119 | return constCase(name) 120 | } 121 | 122 | func privateName(name string) string { 123 | return strings.ToLower(string(name[0])) + name[1:] 124 | } 125 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // UserFlags is the flags of "user" (see UserFlags* consts) 8 | // https://discord.com/developers/docs/resources/user#user-object-user-flags 9 | type UserFlags int 10 | 11 | // Valid UserFlags values 12 | const ( 13 | UserFlagDiscordEmployee UserFlags = 1 << 0 14 | UserFlagDiscordPartner UserFlags = 1 << 1 15 | UserFlagHypeSquadEvents UserFlags = 1 << 2 16 | UserFlagBugHunterLevel1 UserFlags = 1 << 3 17 | UserFlagHouseBravery UserFlags = 1 << 6 18 | UserFlagHouseBrilliance UserFlags = 1 << 7 19 | UserFlagHouseBalance UserFlags = 1 << 8 20 | UserFlagEarlySupporter UserFlags = 1 << 9 21 | UserFlagTeamUser UserFlags = 1 << 10 22 | UserFlagSystem UserFlags = 1 << 12 23 | UserFlagBugHunterLevel2 UserFlags = 1 << 14 24 | UserFlagVerifiedBot UserFlags = 1 << 16 25 | UserFlagVerifiedBotDeveloper UserFlags = 1 << 17 26 | UserFlagDiscordCertifiedModerator UserFlags = 1 << 18 27 | UserFlagBotHTTPInteractions UserFlags = 1 << 19 28 | UserFlagActiveBotDeveloper UserFlags = 1 << 22 29 | ) 30 | 31 | // UserPremiumType is the type of premium (nitro) subscription a user has (see UserPremiumType* consts). 32 | // https://discord.com/developers/docs/resources/user#user-object-premium-types 33 | type UserPremiumType int 34 | 35 | // Valid UserPremiumType values. 36 | const ( 37 | UserPremiumTypeNone UserPremiumType = 0 38 | UserPremiumTypeNitroClassic UserPremiumType = 1 39 | UserPremiumTypeNitro UserPremiumType = 2 40 | UserPremiumTypeNitroBasic UserPremiumType = 3 41 | ) 42 | 43 | // A User stores all data for an individual Discord user. 44 | type User struct { 45 | // The ID of the user. 46 | ID string `json:"id"` 47 | 48 | // The email of the user. This is only present when 49 | // the application possesses the email scope for the user. 50 | Email string `json:"email"` 51 | 52 | // The user's username. 53 | Username string `json:"username"` 54 | 55 | // The hash of the user's avatar. Use Session.UserAvatar 56 | // to retrieve the avatar itself. 57 | Avatar string `json:"avatar"` 58 | 59 | // The user's chosen language option. 60 | Locale string `json:"locale"` 61 | 62 | // The discriminator of the user (4 numbers after name). 63 | Discriminator string `json:"discriminator"` 64 | 65 | // The user's display name, if it is set. 66 | // For bots, this is the application name. 67 | GlobalName string `json:"global_name"` 68 | 69 | // The token of the user. This is only present for 70 | // the user represented by the current session. 71 | Token string `json:"token"` 72 | 73 | // Whether the user's email is verified. 74 | Verified bool `json:"verified"` 75 | 76 | // Whether the user has multi-factor authentication enabled. 77 | MFAEnabled bool `json:"mfa_enabled"` 78 | 79 | // The hash of the user's banner image. 80 | Banner string `json:"banner"` 81 | 82 | // User's banner color, encoded as an integer representation of hexadecimal color code 83 | AccentColor int `json:"accent_color"` 84 | 85 | // Whether the user is a bot. 86 | Bot bool `json:"bot"` 87 | 88 | // The public flags on a user's account. 89 | // This is a combination of bit masks; the presence of a certain flag can 90 | // be checked by performing a bitwise AND between this int and the flag. 91 | PublicFlags UserFlags `json:"public_flags"` 92 | 93 | // The type of Nitro subscription on a user's account. 94 | // Only available when the request is authorized via a Bearer token. 95 | PremiumType UserPremiumType `json:"premium_type"` 96 | 97 | // Whether the user is an Official Discord System user (part of the urgent message system). 98 | System bool `json:"system"` 99 | 100 | // The flags on a user's account. 101 | // Only available when the request is authorized via a Bearer token. 102 | Flags int `json:"flags"` 103 | } 104 | 105 | // String returns a unique identifier of the form username#discriminator 106 | // or just username, if the discriminator is set to "0". 107 | func (u *User) String() string { 108 | // If the user has been migrated from the legacy username system, their discriminator is "0". 109 | // See https://support-dev.discord.com/hc/en-us/articles/13667755828631 110 | if u.Discriminator == "0" { 111 | return u.Username 112 | } 113 | 114 | return u.Username + "#" + u.Discriminator 115 | } 116 | 117 | // Mention return a string which mentions the user 118 | func (u *User) Mention() string { 119 | return "<@" + u.ID + ">" 120 | } 121 | 122 | // AvatarURL returns a URL to the user's avatar. 123 | // 124 | // size: The size of the user's avatar as a power of two 125 | // if size is an empty string, no size parameter will 126 | // be added to the URL. 127 | func (u *User) AvatarURL(size string) string { 128 | return avatarURL( 129 | u.Avatar, 130 | EndpointDefaultUserAvatar(u.DefaultAvatarIndex()), 131 | EndpointUserAvatar(u.ID, u.Avatar), 132 | EndpointUserAvatarAnimated(u.ID, u.Avatar), 133 | size, 134 | ) 135 | } 136 | 137 | // BannerURL returns the URL of the users's banner image. 138 | // 139 | // size: The size of the desired banner image as a power of two 140 | // Image size can be any power of two between 16 and 4096. 141 | func (u *User) BannerURL(size string) string { 142 | return bannerURL(u.Banner, EndpointUserBanner(u.ID, u.Banner), EndpointUserBannerAnimated(u.ID, u.Banner), size) 143 | } 144 | 145 | // DefaultAvatarIndex returns the index of the user's default avatar. 146 | func (u *User) DefaultAvatarIndex() int { 147 | if u.Discriminator == "0" { 148 | id, _ := strconv.ParseUint(u.ID, 10, 64) 149 | return int((id >> 22) % 6) 150 | } 151 | 152 | id, _ := strconv.Atoi(u.Discriminator) 153 | return id % 5 154 | } 155 | 156 | // DisplayName returns the user's global name if they have one, otherwise it returns their username. 157 | func (u *User) DisplayName() string { 158 | if u.GlobalName != "" { 159 | return u.GlobalName 160 | } 161 | return u.Username 162 | } 163 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import "testing" 4 | 5 | func TestUser_String(t *testing.T) { 6 | t.Parallel() 7 | 8 | tests := []struct { 9 | name string 10 | u *User 11 | want string 12 | }{ 13 | { 14 | name: "User with a discriminator", 15 | u: &User{ 16 | Username: "bob", 17 | Discriminator: "8192", 18 | }, 19 | want: "bob#8192", 20 | }, 21 | { 22 | name: "User with discriminator set to 0", 23 | u: &User{ 24 | Username: "aldiwildan", 25 | Discriminator: "0", 26 | }, 27 | want: "aldiwildan", 28 | }, 29 | } 30 | for _, tc := range tests { 31 | t.Run(tc.name, func(t *testing.T) { 32 | if got := tc.u.String(); got != tc.want { 33 | t.Errorf("User.String() = %v, want %v", got, tc.want) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestUser_DisplayName(t *testing.T) { 40 | t.Run("no global name set", func(t *testing.T) { 41 | u := &User{ 42 | GlobalName: "", 43 | Username: "username", 44 | } 45 | if dn := u.DisplayName(); dn != u.Username { 46 | t.Errorf("User.DisplayName() = %v, want %v", dn, u.Username) 47 | } 48 | }) 49 | t.Run("global name set", func(t *testing.T) { 50 | u := &User{ 51 | GlobalName: "global", 52 | Username: "username", 53 | } 54 | if dn := u.DisplayName(); dn != u.GlobalName { 55 | t.Errorf("User.DisplayName() = %v, want %v", dn, u.GlobalName) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "mime/multipart" 8 | "net/textproto" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // SnowflakeTimestamp returns the creation time of a Snowflake ID relative to the creation of Discord. 15 | func SnowflakeTimestamp(ID string) (t time.Time, err error) { 16 | i, err := strconv.ParseInt(ID, 10, 64) 17 | if err != nil { 18 | return 19 | } 20 | timestamp := (i >> 22) + 1420070400000 21 | t = time.Unix(0, timestamp*1000000) 22 | return 23 | } 24 | 25 | // MultipartBodyWithJSON returns the contentType and body for a discord request 26 | // data : The object to encode for payload_json in the multipart request 27 | // files : Files to include in the request 28 | func MultipartBodyWithJSON(data interface{}, files []*File) (requestContentType string, requestBody []byte, err error) { 29 | body := &bytes.Buffer{} 30 | bodywriter := multipart.NewWriter(body) 31 | 32 | payload, err := Marshal(data) 33 | if err != nil { 34 | return 35 | } 36 | 37 | var p io.Writer 38 | 39 | h := make(textproto.MIMEHeader) 40 | h.Set("Content-Disposition", `form-data; name="payload_json"`) 41 | h.Set("Content-Type", "application/json") 42 | 43 | p, err = bodywriter.CreatePart(h) 44 | if err != nil { 45 | return 46 | } 47 | 48 | if _, err = p.Write(payload); err != nil { 49 | return 50 | } 51 | 52 | for i, file := range files { 53 | h := make(textproto.MIMEHeader) 54 | h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="files[%d]"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) 55 | contentType := file.ContentType 56 | if contentType == "" { 57 | contentType = "application/octet-stream" 58 | } 59 | h.Set("Content-Type", contentType) 60 | 61 | p, err = bodywriter.CreatePart(h) 62 | if err != nil { 63 | return 64 | } 65 | 66 | if _, err = io.Copy(p, file.Reader); err != nil { 67 | return 68 | } 69 | } 70 | 71 | err = bodywriter.Close() 72 | if err != nil { 73 | return 74 | } 75 | 76 | return bodywriter.FormDataContentType(), body.Bytes(), nil 77 | } 78 | 79 | func avatarURL(avatarHash, defaultAvatarURL, staticAvatarURL, animatedAvatarURL, size string) string { 80 | var URL string 81 | if avatarHash == "" { 82 | URL = defaultAvatarURL 83 | } else if strings.HasPrefix(avatarHash, "a_") { 84 | URL = animatedAvatarURL 85 | } else { 86 | URL = staticAvatarURL 87 | } 88 | 89 | if size != "" { 90 | return URL + "?size=" + size 91 | } 92 | return URL 93 | } 94 | 95 | func bannerURL(bannerHash, staticBannerURL, animatedBannerURL, size string) string { 96 | var URL string 97 | if bannerHash == "" { 98 | return "" 99 | } else if strings.HasPrefix(bannerHash, "a_") { 100 | URL = animatedBannerURL 101 | } else { 102 | URL = staticBannerURL 103 | } 104 | 105 | if size != "" { 106 | return URL + "?size=" + size 107 | } 108 | return URL 109 | } 110 | 111 | func iconURL(iconHash, staticIconURL, animatedIconURL, size string) string { 112 | var URL string 113 | if iconHash == "" { 114 | return "" 115 | } else if strings.HasPrefix(iconHash, "a_") { 116 | URL = animatedIconURL 117 | } else { 118 | URL = staticIconURL 119 | } 120 | 121 | if size != "" { 122 | return URL + "?size=" + size 123 | } 124 | return URL 125 | } 126 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestSnowflakeTimestamp(t *testing.T) { 9 | // #discordgo channel ID :) 10 | id := "155361364909621248" 11 | parsedTimestamp, err := SnowflakeTimestamp(id) 12 | 13 | if err != nil { 14 | t.Errorf("returned error incorrect: got %v, want nil", err) 15 | } 16 | 17 | correctTimestamp := time.Date(2016, time.March, 4, 17, 10, 35, 869*1000000, time.UTC) 18 | if !parsedTimestamp.Equal(correctTimestamp) { 19 | t.Errorf("parsed time incorrect: got %v, want %v", parsedTimestamp, correctTimestamp) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webhook.go: -------------------------------------------------------------------------------- 1 | package discordgo 2 | 3 | // Webhook stores the data for a webhook. 4 | type Webhook struct { 5 | ID string `json:"id"` 6 | Type WebhookType `json:"type"` 7 | GuildID string `json:"guild_id"` 8 | ChannelID string `json:"channel_id"` 9 | User *User `json:"user"` 10 | Name string `json:"name"` 11 | Avatar string `json:"avatar"` 12 | Token string `json:"token"` 13 | 14 | // ApplicationID is the bot/OAuth2 application that created this webhook 15 | ApplicationID string `json:"application_id,omitempty"` 16 | } 17 | 18 | // WebhookType is the type of Webhook (see WebhookType* consts) in the Webhook struct 19 | // https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types 20 | type WebhookType int 21 | 22 | // Valid WebhookType values 23 | const ( 24 | WebhookTypeIncoming WebhookType = 1 25 | WebhookTypeChannelFollower WebhookType = 2 26 | ) 27 | 28 | // WebhookParams is a struct for webhook params, used in the WebhookExecute command. 29 | type WebhookParams struct { 30 | Content string `json:"content,omitempty"` 31 | Username string `json:"username,omitempty"` 32 | AvatarURL string `json:"avatar_url,omitempty"` 33 | TTS bool `json:"tts,omitempty"` 34 | Files []*File `json:"-"` 35 | Components []MessageComponent `json:"components"` 36 | Embeds []*MessageEmbed `json:"embeds,omitempty"` 37 | Attachments []*MessageAttachment `json:"attachments,omitempty"` 38 | AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` 39 | // Only MessageFlagsSuppressEmbeds and MessageFlagsEphemeral can be set. 40 | // MessageFlagsEphemeral can only be set when using Followup Message Create endpoint. 41 | Flags MessageFlags `json:"flags,omitempty"` 42 | // Name of the thread to create. 43 | // NOTE: can only be set if the webhook channel is a forum. 44 | ThreadName string `json:"thread_name,omitempty"` 45 | } 46 | 47 | // WebhookEdit stores data for editing of a webhook message. 48 | type WebhookEdit struct { 49 | Content *string `json:"content,omitempty"` 50 | Components *[]MessageComponent `json:"components,omitempty"` 51 | Embeds *[]*MessageEmbed `json:"embeds,omitempty"` 52 | Files []*File `json:"-"` 53 | Attachments *[]*MessageAttachment `json:"attachments,omitempty"` 54 | AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` 55 | } 56 | --------------------------------------------------------------------------------