forked from andersfylling/disgord
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gateway.go
328 lines (267 loc) · 9.99 KB
/
gateway.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
package disgord
import (
"context"
"errors"
"fmt"
"github.com/andersfylling/disgord/internal/gateway"
"github.com/andersfylling/disgord/internal/gateway/cmd"
"github.com/andersfylling/disgord/internal/httd"
"github.com/andersfylling/disgord/json"
)
func (c *Client) Gateway() GatewayQueryBuilder {
return &gatewayQueryBuilder{client: c, ctx: context.Background(), socketHandlerRegister: socketHandlerRegister{reactor: c.dispatcher}}
}
type GatewayQueryBuilder interface {
WithContext(ctx context.Context) GatewayQueryBuilder
Get() (gateway *gateway.Gateway, err error)
GetBot() (gateway *gateway.GatewayBot, err error)
BotReady(func())
BotGuildsReady(func())
Dispatch(name gatewayCmdName, payload gateway.CmdPayload) (unchandledGuildIDs []Snowflake, err error)
// Connect establishes a websocket connection to the discord API
Connect() error
StayConnectedUntilInterrupted() error
// Disconnect closes the discord websocket connection
Disconnect() error
DisconnectOnInterrupt() error
SocketHandlerRegistrator
}
type gatewayQueryBuilder struct {
ctx context.Context
client *Client
socketHandlerRegister
}
func (g gatewayQueryBuilder) WithContext(ctx context.Context) GatewayQueryBuilder {
g.ctx = ctx
return &g
}
// Connect establishes a websocket connection to the discord API
func (g gatewayQueryBuilder) Connect() (err error) {
// set the user ID upon connection
// only works for socketing
//
// also verifies that the correct credentials were supplied
// Avoid races during connection setup
g.client.mu.Lock()
defer g.client.mu.Unlock()
if err = gateway.ConfigureShardConfig(g.ctx, helperGatewayBotGetter{g.client}, &g.client.config.ShardConfig); err != nil {
return err
}
shardMngrConf := gateway.ShardManagerConfig{
HTTPClient: g.client.WebsocketHttpClient,
ShardConfig: g.client.config.ShardConfig,
Logger: g.client.config.Logger,
ShutdownChan: g.client.config.shutdownChan,
IgnoreEvents: g.client.config.RejectEvents,
Intents: g.client.config.DMIntents,
EventChan: g.client.eventChan,
DisgordInfo: LibraryInfo(),
ProjectName: g.client.config.ProjectName,
BotToken: g.client.config.BotToken,
}
if g.client.config.Presence != nil {
if g.client.config.Presence.Status == "" {
g.client.config.Presence.Status = StatusOnline // default
}
shardMngrConf.DefaultBotPresence = g.client.config.Presence
}
sharding := gateway.NewShardMngr(shardMngrConf)
g.client.setupConnectEnv()
g.client.log.Info("Connecting to discord Gateway")
if err = sharding.Connect(); err != nil {
g.client.log.Info(err)
return err
}
g.client.log.Info("Connected")
g.client.shardManager = sharding
return nil
}
// Disconnect closes the discord websocket connection
func (g gatewayQueryBuilder) Disconnect() (err error) {
fmt.Println() // to keep ^C on it's own line
g.client.log.Info("Closing Discord gateway connection")
close(g.client.dispatcher.shutdown)
if err = g.client.shardManager.Disconnect(); err != nil {
g.client.log.Error(err)
return err
}
close(g.client.shutdownChan)
g.client.log.Info("Disconnected")
return nil
}
// DisconnectOnInterrupt wait until a termination signal is detected
func (g gatewayQueryBuilder) DisconnectOnInterrupt() (err error) {
// catches panic when being called as a deferred function
if r := recover(); r != nil {
panic("unable to connect due to above error")
}
<-CreateTermSigListener()
return g.Disconnect()
}
// StayConnectedUntilInterrupted is a simple wrapper for connect, and disconnect that listens for system interrupts.
// When a error happens you can terminate the application without worries.
func (g gatewayQueryBuilder) StayConnectedUntilInterrupted() (err error) {
// catches panic when being called as a deferred function
if r := recover(); r != nil {
panic("unable to connect due to above error")
}
if err = g.Connect(); err != nil {
g.client.log.Error(err)
return err
}
ctx := g.ctx
if ctx == nil {
ctx = context.Background()
}
select {
case <-CreateTermSigListener():
case <-ctx.Done():
}
return g.Disconnect()
}
// BotReady triggers a given callback when all shards has gotten their first Ready event
// Warning: Do not call Client.Connect before this.
func (g gatewayQueryBuilder) BotReady(cb func()) {
ctrl := &rdyCtrl{
cb: cb,
}
g.WithCtrl(ctrl).Ready(func(_ Session, evt *Ready) {
ctrl.Lock()
defer ctrl.Unlock()
l := g.client.shardManager.ShardCount()
if l != uint(len(ctrl.shardReady)) {
ctrl.shardReady = make([]bool, l)
ctrl.localShardIDs = g.client.shardManager.ShardIDs()
}
ctrl.shardReady[evt.ShardID] = true
})
}
// BotGuildsReady is triggered once all unavailable Guilds given in the READY event has loaded from their respective GUILD_CREATE events.
func (g gatewayQueryBuilder) BotGuildsReady(cb func()) {
ctrl := &guildsRdyCtrl{
status: make(map[Snowflake]bool),
}
ctrl.cb = cb
ctrl.status[0] = false
g.WithCtrl(ctrl).Ready(func(_ Session, evt *Ready) {
ctrl.Lock()
defer ctrl.Unlock()
for _, g := range evt.Guilds {
if _, ok := ctrl.status[g.ID]; !ok {
ctrl.status[g.ID] = false
}
}
delete(ctrl.status, 0)
})
g.WithCtrl(ctrl).GuildCreate(func(_ Session, evt *GuildCreate) {
ctrl.Lock()
defer ctrl.Unlock()
ctrl.status[evt.Guild.ID] = true
})
}
// Emit sends a socket command directly to Discord.
func (g gatewayQueryBuilder) Dispatch(name gatewayCmdName, payload gateway.CmdPayload) (unchandledGuildIDs []Snowflake, err error) {
g.client.mu.RLock()
defer g.client.mu.RUnlock()
if g.client.shardManager == nil {
return nil, errors.New("you must connect before you can Dispatch requests")
}
return g.client.shardManager.Emit(string(name), payload)
}
// Get Returns an object with a single valid WSS URL, which the Client can use for Connecting.
// Clients should cacheLink this value and only call this endpoint to retrieve a new URL if they are unable to
// properly establish a connection using the cached version of the URL.
// Method GET
// Endpoint /gateway
// Discord documentation https://discord.com/developers/docs/topics/gateway#get-gateway
// Reviewed 2018-10-12
// Comment This endpoint does not require authentication.
func (g gatewayQueryBuilder) Get() (gateway *gateway.Gateway, err error) {
var body []byte
_, body, err = g.client.req.Do(g.ctx, &httd.Request{
Method: httd.MethodGet,
Endpoint: "/gateway",
})
if err != nil {
return
}
err = json.Unmarshal(body, &gateway)
if gateway.URL, err = ensureDiscordGatewayURLHasQueryParams(gateway.URL); err != nil {
return gateway, err
}
return
}
// GetBot Returns an object based on the information in Get Gateway, plus additional metadata
// that can help during the operation of large or sharded bots. Unlike the Get Gateway, this route should not
// be cached for extended periods of time as the value is not guaranteed to be the same per-call, and
// changes as the bot joins/leaves Guilds.
// Method GET
// Endpoint /gateway/bot
// Discord documentation https://discord.com/developers/docs/topics/gateway#get-gateway-bot
// Reviewed 2018-10-12
// Comment This endpoint requires authentication using a valid bot token.
func (g gatewayQueryBuilder) GetBot() (gateway *gateway.GatewayBot, err error) {
var body []byte
_, body, err = g.client.req.Do(g.ctx, &httd.Request{
Method: httd.MethodGet,
Endpoint: "/gateway/bot",
})
if err != nil {
return
}
if err = json.Unmarshal(body, &gateway); err != nil {
return nil, err
}
if gateway.URL, err = ensureDiscordGatewayURLHasQueryParams(gateway.URL); err != nil {
return gateway, err
}
return gateway, nil
}
// ##############################################################################################################
// gatewayCmdName is the gateway command name for the payload to be sent to Discord over a websocket connection.
type gatewayCmdName string
const (
// GatewayCmdRequestGuildMembers Used to request offline members for a guild or
// a list of Guilds. When initially connecting, the gateway will only send
// offline members if a guild has less than the large_threshold members
// (value in the Gateway Identify). If a Client wishes to receive additional
// members, they need to explicitly request them via this operation. The
// server will send Guild Members Chunk events in response with up to 1000
// members per chunk until all members that match the request have been sent.
RequestGuildMembers gatewayCmdName = cmd.RequestGuildMembers
// UpdateVoiceState Sent when a Client wants to join, move, or
// disconnect from a voice channel.
UpdateVoiceState gatewayCmdName = cmd.UpdateVoiceState
// UpdateStatus Sent by the Client to indicate a presence or status
// update.
UpdateStatus gatewayCmdName = cmd.UpdateStatus
)
// #################################################################
// RequestGuildMembersPayload payload for socket command REQUEST_GUILD_MEMBERS.
// See RequestGuildMembers
//
// WARNING: If this request is in queue while a auto-scaling is forced, it will be removed from the queue
// and not re-inserted like the other commands. This is due to the guild id slice, which is a bit trickier
// to handle.
//
// Wrapper for websocket.RequestGuildMembersPayload
type RequestGuildMembersPayload = gateway.RequestGuildMembersPayload
var _ gateway.CmdPayload = (*RequestGuildMembersPayload)(nil)
// UpdateVoiceStatePayload payload for socket command UPDATE_VOICE_STATE.
// see UpdateVoiceState
//
// Wrapper for websocket.UpdateVoiceStatePayload
type UpdateVoiceStatePayload = gateway.UpdateVoiceStatePayload
var _ gateway.CmdPayload = (*UpdateVoiceStatePayload)(nil)
const (
StatusOnline = gateway.StatusOnline
StatusOffline = gateway.StatusOffline
StatusDnd = gateway.StatusDND
StatusIdle = gateway.StatusIdle
)
// UpdateStatusPayload payload for socket command UPDATE_STATUS.
// see UpdateStatus
//
// Wrapper for websocket.UpdateStatusPayload
type UpdateStatusPayload = gateway.UpdateStatusPayload
var _ gateway.CmdPayload = (*UpdateStatusPayload)(nil)