├── .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 | [](https://pkg.go.dev/github.com/bwmarrin/discordgo) [](https://goreportcard.com/report/github.com/bwmarrin/discordgo) [](https://github.com/bwmarrin/discordgo/actions/workflows/ci.yml) [](https://discord.gg/golang) [](https://discord.com/invite/discord-api)
4 |
5 |
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 | - [](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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------