discordrb 2.1.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of discordrb might be problematic. Click here for more details.

@@ -0,0 +1,134 @@
1
+ # API calls for User object
2
+ module Discordrb::API::User
3
+ module_function
4
+
5
+ # Returns users based on a query
6
+ # https://discordapp.com/developers/docs/resources/user#query-users
7
+ def query(token, query, limit = nil)
8
+ Discordrb::API.request(
9
+ :users,
10
+ nil,
11
+ :get,
12
+ "#{Discordrb::API.api_base}/users?q=#{query}#{"&limit=#{limit}" if limit}",
13
+ Authorization: token
14
+ )
15
+ end
16
+
17
+ # Get user data
18
+ # https://discordapp.com/developers/docs/resources/user#get-user
19
+ def resolve(token, user_id)
20
+ Discordrb::API.request(
21
+ :users_uid,
22
+ nil,
23
+ :get,
24
+ "#{Discordrb::API.api_base}/users/#{user_id}",
25
+ Authorization: token
26
+ )
27
+ end
28
+
29
+ # Get profile data
30
+ # https://discordapp.com/developers/docs/resources/user#get-current-user
31
+ def profile(token)
32
+ Discordrb::API.request(
33
+ :users_me,
34
+ nil,
35
+ :get,
36
+ "#{Discordrb::API.api_base}/users/@me",
37
+ Authorization: token
38
+ )
39
+ end
40
+
41
+ # Change the current bot's nickname on a server
42
+ def change_own_nickname(token, server_id, nick)
43
+ Discordrb::API.request(
44
+ :guilds_sid_members_me_nick,
45
+ server_id, # This is technically a guild endpoint
46
+ :patch,
47
+ "#{Discordrb::API.api_base}/guilds/#{server_id}/members/@me/nick",
48
+ { nick: nick }.to_json,
49
+ Authorization: token,
50
+ content_type: :json
51
+ )
52
+ end
53
+
54
+ # Update user data
55
+ # https://discordapp.com/developers/docs/resources/user#modify-current-user
56
+ def update_profile(token, email, password, new_username, avatar, new_password = nil)
57
+ Discordrb::API.request(
58
+ :users_me,
59
+ nil,
60
+ :patch,
61
+ "#{Discordrb::API.api_base}/users/@me",
62
+ { avatar: avatar, email: email, new_password: new_password, password: password, username: new_username }.to_json,
63
+ Authorization: token,
64
+ content_type: :json
65
+ )
66
+ end
67
+
68
+ # Get the servers a user is connected to
69
+ # https://discordapp.com/developers/docs/resources/user#get-current-user-guilds
70
+ def servers(token)
71
+ Discordrb::API.request(
72
+ :users_me_guilds,
73
+ nil,
74
+ :get,
75
+ "#{Discordrb::API.api_base}/users/@me/guilds",
76
+ Authorization: token
77
+ )
78
+ end
79
+
80
+ # Leave a server
81
+ # https://discordapp.com/developers/docs/resources/user#leave-guild
82
+ def leave_server(token, server_id)
83
+ Discordrb::API.request(
84
+ :users_me_guilds_sid,
85
+ nil,
86
+ :delete,
87
+ "#{Discordrb::API.api_base}/users/@me/guilds/#{server_id}",
88
+ Authorization: token
89
+ )
90
+ end
91
+
92
+ # Get the DMs for the current user
93
+ # https://discordapp.com/developers/docs/resources/user#get-user-dms
94
+ def user_dms(token)
95
+ Discordrb::API.request(
96
+ :users_me_channels,
97
+ nil,
98
+ :get,
99
+ "#{Discordrb::API.api_base}/users/@me/channels",
100
+ Authorization: token
101
+ )
102
+ end
103
+
104
+ # Create a DM to another user
105
+ # https://discordapp.com/developers/docs/resources/user#create-dm
106
+ def create_pm(token, recipient_id)
107
+ Discordrb::API.request(
108
+ :users_me_channels,
109
+ nil,
110
+ :post,
111
+ "#{Discordrb::API.api_base}/users/@me/channels",
112
+ { recipient_id: recipient_id }.to_json,
113
+ Authorization: token,
114
+ content_type: :json
115
+ )
116
+ end
117
+
118
+ # Get information about a user's connections
119
+ # https://discordapp.com/developers/docs/resources/user#get-users-connections
120
+ def connections(token)
121
+ Discordrb::API.request(
122
+ :users_me_connections,
123
+ nil,
124
+ :get,
125
+ "#{Discordrb::API.api_base}/users/@me/connections",
126
+ Authorization: token
127
+ )
128
+ end
129
+
130
+ # Make an avatar URL from the user and avatar IDs
131
+ def avatar_url(user_id, avatar_id)
132
+ "#{Discordrb::API.api_base}/users/#{user_id}/avatars/#{avatar_id}.jpg"
133
+ end
134
+ end
data/lib/discordrb/bot.rb CHANGED
@@ -17,78 +17,22 @@ require 'discordrb/events/await'
17
17
  require 'discordrb/events/bans'
18
18
 
19
19
  require 'discordrb/api'
20
+ require 'discordrb/api/channel'
21
+ require 'discordrb/api/server'
22
+ require 'discordrb/api/invite'
20
23
  require 'discordrb/errors'
21
24
  require 'discordrb/data'
22
25
  require 'discordrb/await'
23
- require 'discordrb/token_cache'
24
26
  require 'discordrb/container'
25
27
  require 'discordrb/websocket'
26
28
  require 'discordrb/cache'
29
+ require 'discordrb/gateway'
27
30
 
28
31
  require 'discordrb/voice/voice_bot'
29
32
 
30
33
  module Discordrb
31
- # Gateway packet opcodes
32
- module Opcodes
33
- # **Received** when Discord dispatches an event to the gateway (like MESSAGE_CREATE, PRESENCE_UPDATE or whatever).
34
- # The vast majority of received packets will have this opcode.
35
- DISPATCH = 0
36
-
37
- # **Two-way**: The client has to send a packet with this opcode every ~40 seconds (actual interval specified in
38
- # READY or RESUMED) and the current sequence number, otherwise it will be disconnected from the gateway. In certain
39
- # cases Discord may also send one, specifically if two clients are connected at once.
40
- HEARTBEAT = 1
41
-
42
- # **Sent**: This is one of the two possible ways to initiate a session after connecting to the gateway. It
43
- # should contain the authentication token along with other stuff the server has to know right from the start, such
44
- # as large_threshold and, for older gateway versions, the desired version.
45
- IDENTIFY = 2
46
-
47
- # **Sent**: Packets with this opcode are used to change the user's status and played game. (Sending this is never
48
- # necessary for a gateway client to behave correctly)
49
- PRESENCE = 3
50
-
51
- # **Sent**: Packets with this opcode are used to change a user's voice state (mute/deaf/unmute/undeaf/etc.). It is
52
- # also used to connect to a voice server in the first place. (Sending this is never necessary for a gateway client
53
- # to behave correctly)
54
- VOICE_STATE = 4
55
-
56
- # **Sent**: This opcode is used to ping a voice server, whatever that means. The functionality of this opcode isn't
57
- # known well but non-user clients should never send it.
58
- VOICE_PING = 5
59
-
60
- # **Sent**: This is the other of two possible ways to initiate a gateway session (other than {IDENTIFY}). Rather
61
- # than starting an entirely new session, it resumes an existing session by replaying all events from a given
62
- # sequence number. It should be used to recover from a connection error or anything like that when the session is
63
- # still valid - sending this with an invalid session will cause an error to occur.
64
- RESUME = 6
65
-
66
- # **Received**: Discord sends this opcode to indicate that the client should reconnect to a different gateway
67
- # server because the old one is currently being decommissioned. Counterintuitively, this opcode also invalidates the
68
- # session - the client has to create an entirely new session with the new gateway instead of resuming the old one.
69
- RECONNECT = 7
70
-
71
- # **Sent**: This opcode identifies packets used to retrieve a list of members from a particular server. There is
72
- # also a REST endpoint available for this, but it is inconvenient to use because the client has to implement
73
- # pagination itself, whereas sending this opcode lets Discord handle the pagination and the client can just add
74
- # members when it receives them. (Sending this is never necessary for a gateway client to behave correctly)
75
- REQUEST_MEMBERS = 8
76
-
77
- # **Received**: The functionality of this opcode is less known than the others but it appears to specifically
78
- # tell the client to invalidate its local session and continue by {IDENTIFY}ing.
79
- INVALIDATE_SESSION = 9
80
- end
81
-
82
34
  # Represents a Discord bot, including servers, users, etc.
83
35
  class Bot
84
- # The list of users the bot shares a server with.
85
- # @return [Hash<Integer => User>] The users by ID.
86
- attr_reader :users
87
-
88
- # The list of servers the bot is currently in.
89
- # @return [Hash<Integer => Server>] The servers by ID.
90
- attr_reader :servers
91
-
92
36
  # The list of currently running threads used to parse and call events.
93
37
  # The threads will have a local variable `:discordrb_name` in the format of `et-1234`, where
94
38
  # "et" stands for "event thread" and the number is a continually incrementing number representing
@@ -96,12 +40,6 @@ module Discordrb
96
40
  # @return [Array<Thread>] The threads.
97
41
  attr_reader :event_threads
98
42
 
99
- # The bot's user profile. This special user object can be used
100
- # to edit user data like the current username (see {Profile#username=}).
101
- # @return [Profile] The bot's profile that can be used to edit data.
102
- attr_reader :profile
103
- alias_method :bot_user, :profile
104
-
105
43
  # Whether or not the bot should parse its own messages. Off by default.
106
44
  attr_accessor :should_parse_self
107
45
 
@@ -115,33 +53,30 @@ module Discordrb
115
53
  # @return [Hash<Symbol => Await>] the list of registered {Await}s.
116
54
  attr_reader :awaits
117
55
 
56
+ # The gateway connection is an internal detail that is useless to most people. It is however essential while
57
+ # debugging or developing discordrb itself, or while writing very custom bots.
58
+ # @return [Gateway] the underlying {Gateway} object.
59
+ attr_reader :gateway
60
+
118
61
  include EventContainer
119
62
  include Cache
120
63
 
121
64
  # Makes a new bot with the given authentication data. It will be ready to be added event handlers to and can
122
65
  # eventually be run with {#run}.
123
66
  #
124
- # Depending on the authentication information present, discordrb will deduce whether you're running on a user or a
125
- # bot account. (Discord recommends using bot accounts whenever possible.) The following sets of authentication
126
- # information are valid:
127
- # * token + application_id (bot account)
128
- # * email + password (user account)
129
- # * email + password + token (user account; the given token will be used for authentication instead of email
130
- # and password)
67
+ # As support for logging in using username and password has been removed in version 3.0.0, only a token login is
68
+ # possible. Be sure to specify the `type` parameter as `:user` if you're logging in as a user.
131
69
  #
132
70
  # Simply creating a bot won't be enough to start sending messages etc. with, only a limited set of methods can
133
71
  # be used after logging in. If you want to do something when the bot has connected successfully, either do it in the
134
72
  # {#ready} event, or use the {#run} method with the :async parameter and do the processing after that.
135
- # @param email [String] The email for your (or the bot's) Discord account.
136
- # @param password [String] The valid password that should be used to log in to the account.
137
73
  # @param log_mode [Symbol] The mode this bot should use for logging. See {Logger#mode=} for a list of modes.
138
74
  # @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
139
75
  # this. If you're logging in as a user, make sure to also set the account type to :user so discordrb doesn't think
140
76
  # you're trying to log in as a bot.
141
- # @param application_id [Integer] If you're logging in as a bot, the bot's application ID.
142
- # @param type [Symbol] This parameter lets you manually overwrite the account type. If this isn't specified, it will
143
- # be determined by checking what other attributes are there. The only use case for this is if you want to log in
144
- # as a user but only with a token. Valid values are :user and :bot.
77
+ # @param client_id [Integer] If you're logging in as a bot, the bot's client ID.
78
+ # @param type [Symbol] This parameter lets you manually overwrite the account type. This needs to be set when
79
+ # logging in as a user, otherwise discordrb will treat you as a bot account. Valid values are `:user` and `:bot`.
145
80
  # @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
146
81
  # trace the source of excessive API requests; it's recommended to set this to something if you make bots that many
147
82
  # people will host on their servers separately.
@@ -155,16 +90,13 @@ module Discordrb
155
90
  # https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
156
91
  # @param num_shards [Integer] The total number of shards that should be running. See
157
92
  # https://github.com/hammerandchisel/discord-api-docs/issues/17 for how to do sharding.
93
+ # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
158
94
  def initialize(
159
- email: nil, password: nil, log_mode: :normal,
160
- token: nil, application_id: nil,
95
+ log_mode: :normal,
96
+ token: nil, client_id: nil, application_id: nil,
161
97
  type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
162
- shard_id: nil, num_shards: nil)
163
- # Make sure people replace the login details in the example files...
164
- if email.is_a?(String) && email.end_with?('example.com')
165
- puts 'You have to replace the login details in the example files with your own!'
166
- exit
167
- end
98
+ shard_id: nil, num_shards: nil, redact_token: true
99
+ )
168
100
 
169
101
  LOGGER.mode = if log_mode.is_a? TrueClass # Specifically check for `true` because people might not have updated yet
170
102
  :debug
@@ -172,15 +104,17 @@ module Discordrb
172
104
  log_mode
173
105
  end
174
106
 
175
- @should_parse_self = parse_self
107
+ LOGGER.token = token if redact_token
176
108
 
177
- @email = email
178
- @password = password
109
+ @should_parse_self = parse_self
179
110
 
180
- @application_id = application_id
111
+ if application_id
112
+ raise ArgumentError, 'Starting with discordrb 3.0.0, the application_id parameter has been renamed to client_id! Make sure to change this in your bot. This check will be removed in 3.1.0.'
113
+ end
181
114
 
182
- @type = determine_account_type(type, email, password, token, application_id)
115
+ @client_id = client_id
183
116
 
117
+ @type = type || :bot
184
118
  @name = name
185
119
 
186
120
  @shard_key = num_shards ? [shard_id, num_shards] : nil
@@ -188,10 +122,8 @@ module Discordrb
188
122
  LOGGER.fancy = fancy_log
189
123
  @prevent_ready = suppress_ready
190
124
 
191
- debug('Creating token cache')
192
- token_cache = Discordrb::TokenCache.new
193
- debug('Token cache created successfully')
194
- @token = login(type, email, password, token, token_cache)
125
+ @token = process_token(@type, token)
126
+ @gateway = Gateway.new(self, @token)
195
127
 
196
128
  init_cache
197
129
 
@@ -202,8 +134,45 @@ module Discordrb
202
134
 
203
135
  @event_threads = []
204
136
  @current_thread = 0
137
+
138
+ @status = :online
139
+ end
140
+
141
+ # The list of users the bot shares a server with.
142
+ # @return [Hash<Integer => User>] The users by ID.
143
+ def users
144
+ gateway_check
145
+ @users
146
+ end
147
+
148
+ # The list of servers the bot is currently in.
149
+ # @return [Hash<Integer => Server>] The servers by ID.
150
+ def servers
151
+ gateway_check
152
+ @servers
205
153
  end
206
154
 
155
+ # The bot's user profile. This special user object can be used
156
+ # to edit user data like the current username (see {Profile#username=}).
157
+ # @return [Profile] The bot's profile that can be used to edit data.
158
+ def profile
159
+ gateway_check
160
+ @profile
161
+ end
162
+
163
+ alias_method :bot_user, :profile
164
+
165
+ # The bot's OAuth application.
166
+ # @return [Application, nil] The bot's application info. Returns `nil` if bot is not a bot account.
167
+ def bot_application
168
+ gateway_check
169
+ return nil unless @type == :bot
170
+ response = API.oauth_application(token)
171
+ Application.new(JSON.parse(response), self)
172
+ end
173
+
174
+ alias_method :bot_app, :bot_application
175
+
207
176
  # The Discord API token received when logging in. Useful to explicitly call
208
177
  # {API} methods.
209
178
  # @return [String] The API token.
@@ -225,85 +194,49 @@ module Discordrb
225
194
  # If the bot is run in async mode, make sure to eventually run {#sync} so
226
195
  # the script doesn't stop prematurely.
227
196
  def run(async = false)
228
- run_async
197
+ @gateway.run_async
229
198
  return if async
230
199
 
231
200
  debug('Oh wait! Not exiting yet as run was run synchronously.')
232
- sync
233
- end
234
-
235
- # Runs the bot asynchronously. Equivalent to #run with the :async parameter.
236
- # @see #run
237
- def run_async
238
- # Handle heartbeats
239
- @heartbeat_interval = 1
240
- @heartbeat_active = false
241
- @heartbeat_thread = Thread.new do
242
- Thread.current[:discordrb_name] = 'heartbeat'
243
- loop do
244
- if @heartbeat_active
245
- send_heartbeat
246
- sleep @heartbeat_interval
247
- else
248
- sleep 1
249
- end
250
- end
251
- end
252
-
253
- @ws_thread = Thread.new do
254
- Thread.current[:discordrb_name] = 'websocket'
255
-
256
- # Initialize falloff so we wait for more time before reconnecting each time
257
- @falloff = 1.0
258
-
259
- loop do
260
- websocket_connect
261
-
262
- if @reconnect_url
263
- # We got an op 7! Don't wait before reconnecting
264
- LOGGER.info('Got an op 7, reconnecting right away')
265
- else
266
- wait_for_reconnect
267
- end
268
-
269
- # Restart the loop, i. e. reconnect
270
- end
271
-
272
- LOGGER.warn('The WS loop exited! Not sure if this is a good thing')
273
- end
274
-
275
- debug('WS thread created! Now waiting for confirmation that everything worked')
276
- @ws_success = false
277
- sleep(0.5) until @ws_success
278
- debug('Confirmation received! Exiting run.')
201
+ @gateway.sync
279
202
  end
280
203
 
281
- # Prevents all further execution until the websocket thread stops (e. g. through a closed connection).
204
+ # Blocks execution until the websocket stops, which should only happen manually triggered
205
+ # or due to an error. This is necessary to have a continuously running bot.
282
206
  def sync
283
- @ws_thread.join
207
+ @gateway.sync
208
+ end
209
+
210
+ # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
211
+ # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
212
+ # @param no_sync [true, false] Whether or not to disable use of synchronize in the close method. This should be true if called from a trap context.
213
+ def stop(no_sync = false)
214
+ @gateway.stop(no_sync)
284
215
  end
285
216
 
286
- # Kills the websocket thread, stopping all connections to Discord.
287
- def stop
288
- @ws_thread.kill
217
+ # @return [true, false] whether or not the bot is currently connected to Discord.
218
+ def connected?
219
+ @gateway.open?
289
220
  end
290
221
 
291
222
  # Makes the bot join an invite to a server.
292
223
  # @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
293
224
  def join(invite)
294
225
  resolved = invite(invite).code
295
- API.join_server(token, resolved)
226
+ API::Invite.accept(token, resolved)
296
227
  end
297
228
 
298
229
  # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
299
230
  # Requires the application ID to have been set during initialization.
300
231
  # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
232
+ # @param permission_bits [Integer, String] Permission bits that should be appended to invite url.
301
233
  # @return [String] the OAuth invite URL.
302
- def invite_url(server = nil)
303
- raise 'No application ID has been set during initialization! Add one as the `application_id` named parameter while creating your bot.' unless @application_id
234
+ def invite_url(server: nil, permission_bits: nil)
235
+ raise 'No application ID has been set during initialization! Add one as the `application_id` named parameter while creating your bot.' unless @client_id
304
236
 
305
- guild_id_str = server ? "&guild_id=#{server.id}" : ''
306
- "https://discordapp.com/oauth2/authorize?&client_id=#{@application_id}#{guild_id_str}&scope=bot"
237
+ server_id_str = server ? "&guild_id=#{server.id}" : ''
238
+ permission_bits_str = permission_bits ? "&permissions=#{permission_bits}" : ''
239
+ "https://discordapp.com/oauth2/authorize?&client_id=#{@client_id}#{server_id_str}#{permission_bits_str}&scope=bot"
307
240
  end
308
241
 
309
242
  # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
@@ -347,19 +280,9 @@ module Discordrb
347
280
 
348
281
  debug("Got voice channel: #{chan}")
349
282
 
350
- data = {
351
- op: Opcodes::VOICE_STATE,
352
- d: {
353
- guild_id: server_id.to_s,
354
- channel_id: chan.id.to_s,
355
- self_mute: false,
356
- self_deaf: false
357
- }
358
- }
359
- debug("Voice channel init packet is: #{data.to_json}")
360
-
361
283
  @should_connect_to_voice[server_id] = chan
362
- @ws.send(data.to_json)
284
+ @gateway.send_voice_state_update(server_id.to_s, chan.id.to_s, false, false)
285
+
363
286
  debug('Voice channel init packet sent! Now waiting.')
364
287
 
365
288
  sleep(0.05) until @voices[server_id]
@@ -373,19 +296,7 @@ module Discordrb
373
296
  # @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
374
297
  # directly, you should leave it as true.
375
298
  def voice_destroy(server_id, destroy_vws = true)
376
- data = {
377
- op: Opcodes::VOICE_STATE,
378
- d: {
379
- guild_id: server_id.to_s,
380
- channel_id: nil,
381
- self_mute: false,
382
- self_deaf: false
383
- }
384
- }
385
-
386
- debug("Voice channel destroy packet is: #{data.to_json}")
387
- @ws.send(data.to_json)
388
-
299
+ @gateway.send_voice_state_update(server_id.to_s, nil, false, false)
389
300
  @voices[server_id].destroy if @voices[server_id] && destroy_vws
390
301
  @voices.delete(server_id)
391
302
  end
@@ -395,28 +306,50 @@ module Discordrb
395
306
  # @param code [String, Invite] The invite to revoke. For possible formats see {#resolve_invite_code}.
396
307
  def delete_invite(code)
397
308
  invite = resolve_invite_code(code)
398
- API.delete_invite(token, invite)
309
+ API::Invite.delete(token, invite)
399
310
  end
400
311
 
401
312
  # Sends a text message to a channel given its ID and the message's content.
402
313
  # @param channel_id [Integer] The ID that identifies the channel to send something to.
403
314
  # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
404
315
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
316
+ # @param server_id [Integer] The ID that identifies the server to send something to.
405
317
  # @return [Message] The message that was sent.
406
318
  def send_message(channel_id, content, tts = false, server_id = nil)
407
319
  channel_id = channel_id.resolve_id
408
320
  debug("Sending message to #{channel_id} with content '#{content}'")
409
321
 
410
- response = API.send_message(token, channel_id, content, [], tts, server_id)
322
+ response = API::Channel.create_message(token, channel_id, content, [], tts, server_id)
411
323
  Message.new(JSON.parse(response), self)
412
324
  end
413
325
 
326
+ # Sends a text message to a channel given its ID and the message's content,
327
+ # then deletes it after the specified timeout in seconds.
328
+ # @param channel_id [Integer] The ID that identifies the channel to send something to.
329
+ # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
330
+ # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
331
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
332
+ # @param server_id [Integer] The ID that identifies the server to send something to.
333
+ def send_temporary_message(channel_id, content, timeout, tts = false, server_id = nil)
334
+ Thread.new do
335
+ message = send_message(channel_id, content, tts, server_id)
336
+
337
+ sleep(timeout)
338
+
339
+ message.delete
340
+ end
341
+
342
+ nil
343
+ end
344
+
414
345
  # Sends a file to a channel. If it is an image, it will automatically be embedded.
415
346
  # @note This executes in a blocking way, so if you're sending long files, be wary of delays.
416
347
  # @param channel_id [Integer] The ID that identifies the channel to send something to.
417
348
  # @param file [File] The file that should be sent.
418
- def send_file(channel_id, file)
419
- response = API.send_file(token, channel_id, file)
349
+ # @param caption [string] The caption for the file.
350
+ # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
351
+ def send_file(channel_id, file, caption: nil, tts: false)
352
+ response = API::Channel.upload_file(token, channel_id, file, caption: caption, tts: tts)
420
353
  Message.new(JSON.parse(response), self)
421
354
  end
422
355
 
@@ -437,7 +370,7 @@ module Discordrb
437
370
  # * `:sydney`
438
371
  # @return [Server] The server that was created.
439
372
  def create_server(name, region = :london)
440
- response = API.create_server(token, name, region)
373
+ response = API::Server.create(token, name, region)
441
374
  id = JSON.parse(response)['id'].to_i
442
375
  sleep 0.1 until @servers[id]
443
376
  server = @servers[id]
@@ -474,43 +407,70 @@ module Discordrb
474
407
  user(id.to_i)
475
408
  end
476
409
 
410
+ # Updates presence status.
411
+ # @param status [String] The status the bot should show up as.
412
+ # @param game [String, nil] The name of the game to be played/stream name on the stream.
413
+ # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
414
+ # @param since [Integer] When this status was set.
415
+ # @param afk [true, false] Whether the bot is AFK.
416
+ # @see Gateway#send_status_update
417
+ def update_status(status, game, url, since = 0, afk = false)
418
+ gateway_check
419
+
420
+ @game = game
421
+ @status = status
422
+ @streamurl = url
423
+ type = url ? 1 : nil
424
+
425
+ game_obj = game || url ? { name: game, url: url, type: type } : nil
426
+ @gateway.send_status_update(status, since, game_obj, afk)
427
+ end
428
+
477
429
  # Sets the currently playing game to the specified game.
478
430
  # @param name [String] The name of the game to be played.
479
431
  # @return [String] The game that is being played now.
480
432
  def game=(name)
481
- @game = name
482
-
483
- data = {
484
- op: Opcodes::PRESENCE,
485
- d: {
486
- idle_since: nil,
487
- game: name ? { name: name } : nil
488
- }
489
- }
433
+ gateway_check
434
+ update_status(@status, name, nil)
435
+ name
436
+ end
490
437
 
491
- @ws.send(data.to_json)
438
+ # Sets the currently online stream to the specified name and Twitch URL.
439
+ # @param name [String] The name of the stream to display.
440
+ # @param url [String] The url of the current Twitch stream.
441
+ # @return [String] The stream name that is being displayed now.
442
+ def stream(name, url)
443
+ gateway_check
444
+ update_status(@status, name, url)
492
445
  name
493
446
  end
494
447
 
495
- # Injects a reconnect event (op 7) into the event processor, causing Discord to reconnect to the given gateway URL.
496
- # If the URL is set to nil, it will reconnect and get an entirely new gateway URL. This method has not much use
497
- # outside of testing and implementing highly custom reconnect logic.
498
- # @param url [String, nil] the URL to connect to or nil if one should be obtained from Discord.
499
- def inject_reconnect(url)
500
- websocket_message({
501
- op: Opcodes::RECONNECT,
502
- d: {
503
- url: url
504
- }
505
- }.to_json)
448
+ # Sets status to online.
449
+ def online
450
+ gateway_check
451
+ update_status(:online, @game, @streamurl)
452
+ end
453
+
454
+ alias_method :on, :online
455
+
456
+ # Sets status to idle.
457
+ def idle
458
+ gateway_check
459
+ update_status(:idle, @game, nil)
460
+ end
461
+
462
+ alias_method :away, :idle
463
+
464
+ # Sets the bot's status to DnD (red icon).
465
+ def dnd
466
+ gateway_check
467
+ update_status(:dnd, @game, nil)
506
468
  end
507
469
 
508
- # Injects a resume packet (op 6) into the gateway. If this is done with a running connection, it will cause an
509
- # error. It has no use outside of testing stuff that I know of, but if you want to use it anyway for some reason,
510
- # here it is.
511
- # @param seq [Integer, nil] The sequence ID to inject, or nil if the currently tracked one should be used.
512
- def inject_resume(seq)
513
- resume(seq || @sequence, raw_token, @session_id)
470
+ # Sets the bot's status to invisible (appears offline).
471
+ def invisible
472
+ gateway_check
473
+ update_status(:invisible, @game, nil)
514
474
  end
515
475
 
516
476
  # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
@@ -574,26 +534,29 @@ module Discordrb
574
534
  LOGGER.log_exception(e)
575
535
  end
576
536
 
577
- private
537
+ # Dispatches an event to this bot. Called by the gateway connection handler used internally.
538
+ def dispatch(type, data)
539
+ handle_dispatch(type, data)
540
+ end
578
541
 
579
- # Determines the type of an account by checking which parameters are given
580
- def determine_account_type(type, email, password, token, application_id)
581
- # Case 1: if a type is already given, return it
582
- return type if type
542
+ # Raises a heartbeat event. Called by the gateway connection handler used internally.
543
+ def raise_heartbeat_event
544
+ raise_event(HeartbeatEvent.new(self))
545
+ end
583
546
 
584
- # Case 2: user accounts can't have application IDs so if one is given, return bot type
585
- return :bot if application_id
547
+ def prune_empty_groups
548
+ @channels.each_value do |channel|
549
+ channel.leave_group if channel.group? && channel.recipients.empty?
550
+ end
551
+ end
586
552
 
587
- # Case 3: bot accounts can't have emails and passwords so if either is given, assume user
588
- return :user if email || password
553
+ private
589
554
 
590
- # Case 4: If we're here and no token is given, throw an exception because that's impossible
591
- raise ArgumentError, "Can't login because no authentication data was given! Specify at least a token" unless token
555
+ # Throws a useful exception if there's currently no gateway connection
556
+ def gateway_check
557
+ return if connected?
592
558
 
593
- # Case 5: Only a token has been specified, we can assume it's a bot but we should tell the user
594
- # to specify the application ID:
595
- LOGGER.warn('The application ID is missing! Logging in as a bot will work but some OAuth-related functionality will be unavailable!')
596
- :bot
559
+ raise "A gateway connection is necessary to call this method! You'll have to do it inside any event (e.g. `ready`) or after `bot.run :async`."
597
560
  end
598
561
 
599
562
  ### ## ## ######## ######## ######## ## ## ### ## ######
@@ -606,7 +569,7 @@ module Discordrb
606
569
 
607
570
  # Internal handler for PRESENCE_UPDATE
608
571
  def update_presence(data)
609
- # Friends list presences have no guild ID so ignore these to not cause an error
572
+ # Friends list presences have no server ID so ignore these to not cause an error
610
573
  return unless data['guild_id']
611
574
 
612
575
  user_id = data['user']['id'].to_i
@@ -642,28 +605,11 @@ module Discordrb
642
605
 
643
606
  # Internal handler for VOICE_STATUS_UPDATE
644
607
  def update_voice_state(data)
645
- user_id = data['user_id'].to_i
646
608
  server_id = data['guild_id'].to_i
647
609
  server = server(server_id)
648
610
  return unless server
649
611
 
650
- user = server.member(user_id)
651
-
652
- unless user
653
- warn "Invalid user for voice state update: #{user_id} on #{server_id}, ignoring"
654
- return
655
- end
656
-
657
- channel_id = data['channel_id']
658
- channel = nil
659
- channel = self.channel(channel_id.to_i) if channel_id
660
-
661
- user.update_voice_state(
662
- channel,
663
- data['mute'],
664
- data['deaf'],
665
- data['self_mute'],
666
- data['self_deaf'])
612
+ server.update_voice_state(data)
667
613
 
668
614
  @session_id = data['session_id']
669
615
  end
@@ -699,8 +645,10 @@ module Discordrb
699
645
  if server
700
646
  server.channels << channel
701
647
  @channels[channel.id] = channel
702
- else
703
- @private_channels[channel.id] = channel
648
+ elsif channel.pm?
649
+ @pm_channels[channel.recipient.id] = channel
650
+ elsif channel.group?
651
+ @channels[channel.id] = channel
704
652
  end
705
653
  end
706
654
 
@@ -721,11 +669,33 @@ module Discordrb
721
669
  if server
722
670
  @channels.delete(channel.id)
723
671
  server.channels.reject! { |c| c.id == channel.id }
724
- else
725
- @private_channels.delete(channel.id)
672
+ elsif channel.pm?
673
+ @pm_channels.delete(channel.recipient.id)
674
+ elsif channel.group?
675
+ @channels.delete(channel.id)
726
676
  end
727
677
  end
728
678
 
679
+ # Internal handler for CHANNEL_RECIPIENT_ADD
680
+ def add_recipient(data)
681
+ channel_id = data['channel_id'].to_i
682
+ channel = self.channel(channel_id)
683
+
684
+ recipient_user = ensure_user(data['user'])
685
+ recipient = Recipient.new(recipient_user, channel, self)
686
+ channel.add_recipient(recipient)
687
+ end
688
+
689
+ # Internal handler for CHANNEL_RECIPIENT_REMOVE
690
+ def remove_recipient(data)
691
+ channel_id = data['channel_id'].to_i
692
+ channel = self.channel(channel_id)
693
+
694
+ recipient_user = ensure_user(data['user'])
695
+ recipient = Recipient.new(recipient_user, channel, self)
696
+ channel.remove_recipient(recipient)
697
+ end
698
+
729
699
  # Internal handler for GUILD_MEMBER_ADD
730
700
  def add_guild_member(data)
731
701
  server_id = data['guild_id'].to_i
@@ -826,19 +796,6 @@ module Discordrb
826
796
  ## ## ## ## ## ## ## ###
827
797
  ######## ####### ###### #### ## ##
828
798
 
829
- def login(type, email, password, token, token_cache)
830
- # Don't bother with any login code if a token is already specified
831
- return process_token(type, token) if token
832
-
833
- # If a bot account attempts logging in without a token, throw an error
834
- raise ArgumentError, 'Bot account detected (type == :bot) but no token was found! Please specify a token in the Bot initializer, or use a user account.' if type == :bot
835
-
836
- # If the type is not a user account at this point, it must be invalid
837
- raise ArgumentError, 'Invalid type specified! Use either :bot or :user' if type == :user
838
-
839
- user_login(email, password, token_cache)
840
- end
841
-
842
799
  def process_token(type, token)
843
800
  # Remove the "Bot " prefix if it exists
844
801
  token = token[4..-1] if token.start_with? 'Bot '
@@ -847,162 +804,7 @@ module Discordrb
847
804
  token
848
805
  end
849
806
 
850
- def user_login(email, password, token_cache)
851
- debug('Logging in')
852
-
853
- # Attempt to retrieve the token from the cache
854
- retrieved_token = retrieve_token(email, password, token_cache)
855
- return retrieved_token if retrieved_token
856
-
857
- login_attempts ||= 0
858
-
859
- # Login
860
- login_response = JSON.parse(API.login(email, password))
861
- token = login_response['token']
862
- raise Discordrb::Errors::InvalidAuthenticationError unless token
863
- debug('Received token from Discord!')
864
-
865
- # Cache the token
866
- token_cache.store_token(email, password, token)
867
-
868
- token
869
- rescue RestClient::BadRequest
870
- raise Discordrb::Errors::InvalidAuthenticationError
871
- rescue SocketError, RestClient::RequestFailed => e # RequestFailed handles the 52x error codes Cloudflare sometimes sends that aren't covered by specific RestClient classes
872
- if login_attempts && login_attempts > 100
873
- LOGGER.error("User login failed permanently after #{login_attempts} attempts")
874
- raise
875
- else
876
- LOGGER.error("User login failed! Trying again in 5 seconds, #{100 - login_attempts} remaining")
877
- LOGGER.log_exception(e)
878
- retry
879
- end
880
- end
881
-
882
- def retrieve_token(email, password, token_cache)
883
- # First, attempt to get the token from the cache
884
- token = token_cache.token(email, password)
885
- debug('Token successfully obtained from cache!') if token
886
- token
887
- end
888
-
889
- def find_gateway
890
- # If the reconnect URL is set, it means we got an op 7 earlier and should reconnect to the new URL
891
- if @reconnect_url
892
- debug("Reconnecting to URL #{@reconnect_url}")
893
- url = @reconnect_url
894
- @reconnect_url = nil # Unset the URL so we don't connect to the same URL again if the connection fails
895
- url
896
- else
897
- # Get the correct gateway URL from Discord
898
- response = API.gateway(token)
899
- JSON.parse(response)['url']
900
- end
901
- end
902
-
903
- def process_gateway
904
- raw_url = find_gateway
905
-
906
- # Append a slash in case it's not there (I'm not sure how well WSCS handles it otherwise)
907
- raw_url += '/' unless raw_url.end_with? '/'
908
-
909
- # Add the parameters we want
910
- raw_url + "?encoding=json&v=#{GATEWAY_VERSION}"
911
- end
912
-
913
- ## ## ###### ######## ## ## ######## ## ## ######## ######
914
- ## ## ## ## ## ## ## ## ## ### ## ## ## ##
915
- ## ## ## ## ## ## ## ## #### ## ## ##
916
- ## ## ## ###### ###### ## ## ###### ## ## ## ## ######
917
- ## ## ## ## ## ## ## ## ## #### ## ##
918
- ## ## ## ## ## ## ## ## ## ## ### ## ## ##
919
- #### ### ###### ######## ### ######## ## ## ## ######
920
-
921
- # Desired gateway version
922
- GATEWAY_VERSION = 4
923
-
924
- def websocket_connect
925
- debug('Attempting to get gateway URL...')
926
- gateway_url = process_gateway
927
- debug("Success! Gateway URL is #{gateway_url}.")
928
- debug('Now running bot')
929
-
930
- @ws = Discordrb::WebSocket.new(
931
- gateway_url,
932
- method(:websocket_open),
933
- method(:websocket_message),
934
- method(:websocket_close),
935
- proc { |e| LOGGER.error "Gateway error: #{e}" }
936
- )
937
-
938
- @ws.thread[:discordrb_name] = 'gateway'
939
- @ws.thread.join
940
- rescue => e
941
- LOGGER.error 'Error while connecting to the gateway!'
942
- LOGGER.log_exception e
943
- raise
944
- end
945
-
946
- def websocket_reconnect(url)
947
- # In here, we do nothing except set the reconnect URL and close the current connection.
948
- @reconnect_url = url
949
- @ws.close
950
-
951
- # Reset the packet sequence number so we don't try to resume the connection afterwards
952
- @sequence = 0
953
-
954
- # Let's hope the reconnect handler reconnects us correctly...
955
- end
956
-
957
- def websocket_message(event)
958
- if event.byteslice(0) == 'x'
959
- # The message is encrypted
960
- event = Zlib::Inflate.inflate(event)
961
- end
962
-
963
- # Parse packet
964
- packet = JSON.parse(event)
965
-
966
- if @prevent_ready && packet['t'] == 'READY'
967
- debug('READY packet was received and suppressed')
968
- elsif @prevent_ready && packet['t'] == 'GUILD_MEMBERS_CHUNK'
969
- # Ignore chunks as they will be handled later anyway
970
- else
971
- LOGGER.in(event.to_s)
972
- end
973
-
974
- opcode = packet['op'].to_i
975
-
976
- if opcode == Opcodes::HEARTBEAT
977
- # If Discord sends us a heartbeat, simply reply with a heartbeat with the packet's sequence number
978
- @sequence = packet['s'].to_i
979
-
980
- LOGGER.info("Received an op1 (seq: #{@sequence})! This means another client connected while this one is already running. Replying with the same seq")
981
- send_heartbeat
982
-
983
- return
984
- end
985
-
986
- if opcode == Opcodes::RECONNECT
987
- websocket_reconnect(packet['d'] ? packet['d']['url'] : nil)
988
- return
989
- end
990
-
991
- if opcode == Opcodes::INVALIDATE_SESSION
992
- LOGGER.info "We got an opcode 9 from Discord! Invalidating the session. You probably don't have to worry about this."
993
- invalidate_session
994
- LOGGER.debug 'Session invalidated!'
995
-
996
- LOGGER.debug 'Reconnecting with IDENTIFY'
997
- websocket_open # Since we just invalidated the session, pretending we just opened the WS again will re-identify
998
- LOGGER.debug "Re-identified! Let's hope everything works fine."
999
- return
1000
- end
1001
-
1002
- raise "Got an unexpected opcode (#{opcode}) in a gateway event!
1003
- Please report this issue along with the following information:
1004
- v#{GATEWAY_VERSION} #{packet}" unless opcode == Opcodes::DISPATCH
1005
-
807
+ def handle_dispatch(type, data)
1006
808
  # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
1007
809
  if @unavailable_servers && @unavailable_servers > 0 && (Time.now - @unavailable_timeout_time) > 10
1008
810
  # The server streaming timed out!
@@ -1015,26 +817,14 @@ module Discordrb
1015
817
  notify_ready
1016
818
  end
1017
819
 
1018
- # Keep track of the packet sequence (continually incrementing number for every packet) so we can resume a
1019
- # connection if we disconnect
1020
- @sequence = packet['s'].to_i
1021
-
1022
- data = packet['d']
1023
- type = packet['t'].intern
1024
820
  case type
1025
821
  when :READY
1026
- LOGGER.info("Discord using gateway protocol version: #{data['v']}, requested: #{GATEWAY_VERSION}")
822
+ # As READY may be called multiple times over a single process lifetime, we here need to reset the cache entirely
823
+ # to prevent possible inconsistencies, like objects referencing old versions of other objects which have been
824
+ # replaced.
825
+ init_cache
1027
826
 
1028
- # Set the session ID in case we get disconnected and have to resume
1029
- @session_id = data['session_id']
1030
-
1031
- # Activate the heartbeats
1032
- @heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
1033
- @heartbeat_active = true
1034
- debug("Desired heartbeat_interval: #{@heartbeat_interval}")
1035
- send_heartbeat
1036
-
1037
- @profile = Profile.new(data['user'], self, @email, @password)
827
+ @profile = Profile.new(data['user'], self)
1038
828
 
1039
829
  # Initialize servers
1040
830
  @servers = {}
@@ -1055,33 +845,25 @@ module Discordrb
1055
845
  ensure_server(element)
1056
846
  end
1057
847
 
1058
- # Add private channels
848
+ # Add PM and group channels
1059
849
  data['private_channels'].each do |element|
1060
850
  channel = ensure_channel(element)
1061
- @private_channels[channel.recipient.id] = channel
851
+ if channel.pm?
852
+ @pm_channels[channel.recipient.id] = channel
853
+ else
854
+ @channels[channel.id] = channel
855
+ end
1062
856
  end
1063
857
 
1064
858
  # Don't notify yet if there are unavailable servers because they need to get available before the bot truly has
1065
859
  # all the data
1066
- if @unavailable_servers == 0
860
+ if @unavailable_servers.zero?
1067
861
  # No unavailable servers - we're ready!
1068
862
  notify_ready
1069
863
  end
1070
864
 
1071
865
  @ready_time = Time.now
1072
866
  @unavailable_timeout_time = Time.now
1073
- when :RESUMED
1074
- # The RESUMED event is received after a successful op 6 (resume). It does nothing except tell the bot the
1075
- # connection is initiated (like READY would) and set a new heartbeat interval.
1076
- debug('Connection resumed')
1077
-
1078
- @heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
1079
-
1080
- # Since we disabled it earlier so we don't send any heartbeats in between close and resume,, make sure to
1081
- # re-enable heartbeating
1082
- @heartbeat_active = true
1083
-
1084
- debug("Desired heartbeat_interval: #{@heartbeat_interval}")
1085
867
  when :GUILD_MEMBERS_CHUNK
1086
868
  id = data['guild_id'].to_i
1087
869
  server = server(id)
@@ -1128,6 +910,22 @@ module Discordrb
1128
910
 
1129
911
  event = MessageDeleteEvent.new(data, self)
1130
912
  raise_event(event)
913
+ when :MESSAGE_DELETE_BULK
914
+ debug("MESSAGE_DELETE_BULK will raise #{data['ids'].length} events")
915
+
916
+ data['ids'].each do |single_id|
917
+ # Form a data hash for a single ID so the methods get what they want
918
+ single_data = {
919
+ 'id' => single_id,
920
+ 'channel_id' => data['channel_id']
921
+ }
922
+
923
+ # Raise as normal
924
+ delete_message(single_data)
925
+
926
+ event = MessageDeleteEvent.new(single_data, self)
927
+ raise_event(event)
928
+ end
1131
929
  when :TYPING_START
1132
930
  start_typing(data)
1133
931
 
@@ -1177,6 +975,16 @@ module Discordrb
1177
975
 
1178
976
  event = ChannelDeleteEvent.new(data, self)
1179
977
  raise_event(event)
978
+ when :CHANNEL_RECIPIENT_ADD
979
+ add_recipient(data)
980
+
981
+ event = ChannelRecipientAddEvent.new(data, self)
982
+ raise_event(event)
983
+ when :CHANNEL_RECIPIENT_REMOVE
984
+ remove_recipient(data)
985
+
986
+ event = ChannelRecipientRemoveEvent.new(data, self)
987
+ raise_event(event)
1180
988
  when :GUILD_MEMBER_ADD
1181
989
  add_guild_member(data)
1182
990
 
@@ -1225,7 +1033,7 @@ module Discordrb
1225
1033
  @unavailable_servers -= 1 if @unavailable_servers
1226
1034
  @unavailable_timeout_time = Time.now
1227
1035
 
1228
- notify_ready if @unavailable_servers == 0
1036
+ notify_ready if @unavailable_servers.zero?
1229
1037
 
1230
1038
  # Return here so the event doesn't get triggered
1231
1039
  return
@@ -1241,147 +1049,29 @@ module Discordrb
1241
1049
  when :GUILD_DELETE
1242
1050
  delete_guild(data)
1243
1051
 
1052
+ if d['unavailable'].is_a? TrueClass
1053
+ LOGGER.warn("Server #{d['id']} is unavailable due to an outage!")
1054
+ return # Don't raise an event
1055
+ end
1056
+
1244
1057
  event = ServerDeleteEvent.new(data, self)
1245
1058
  raise_event(event)
1246
1059
  else
1247
1060
  # another event that we don't support yet
1248
- debug "Event #{packet['t']} has been received but is unsupported, ignoring"
1061
+ debug "Event #{type} has been received but is unsupported, ignoring"
1249
1062
  end
1250
1063
  rescue Exception => e
1251
1064
  LOGGER.error('Gateway message error!')
1252
1065
  log_exception(e)
1253
1066
  end
1254
1067
 
1255
- def websocket_close(event)
1256
- # Don't handle nil events (for example if the disconnect came from our side)
1257
- return unless event
1258
-
1259
- # Handle actual close frames and errors separately
1260
- if event.respond_to? :code
1261
- LOGGER.error(%(Disconnected from WebSocket - code #{event.code} with reason: "#{event.data}"))
1262
-
1263
- if event.code.to_i == 4006
1264
- # If we got disconnected with a 4006, it means we sent a resume when Discord wanted an identify. To battle this,
1265
- # we invalidate the local session so we'll just send an identify next time
1266
- debug('Apparently we just sent the wrong type of initiation packet (resume rather than identify) to Discord. (Sorry!)
1267
- Invalidating session so this is fixed next time')
1268
- invalidate_session
1269
- end
1270
- else
1271
- LOGGER.error('Disconnected from WebSocket due to an exception!')
1272
- LOGGER.log_exception event
1273
- end
1274
-
1275
- raise_event(DisconnectEvent.new(self))
1276
-
1277
- # Stop sending heartbeats
1278
- @heartbeat_active = false
1279
-
1280
- # Safely close the WS connection and handle any errors that occur there
1281
- begin
1282
- @ws.close
1283
- rescue => e
1284
- LOGGER.warn 'Got the following exception while closing the WS after being disconnected:'
1285
- LOGGER.log_exception e
1286
- end
1287
- rescue => e
1288
- LOGGER.log_exception e
1289
- raise
1290
- end
1291
-
1292
- def websocket_open
1293
- # If we've already received packets (packet sequence > 0) resume an existing connection instead of identifying anew
1294
- if @sequence && @sequence > 0
1295
- resume(@sequence, raw_token, @session_id)
1296
- return
1297
- end
1298
-
1299
- identify(raw_token, 100, GATEWAY_VERSION)
1300
- end
1301
-
1302
- # Identify the client to the gateway
1303
- def identify(token, large_threshold, version)
1304
- # Send the initial packet
1305
- packet = {
1306
- op: Opcodes::IDENTIFY, # Opcode
1307
- d: { # Packet data
1308
- v: version, # WebSocket protocol version
1309
- token: token,
1310
- properties: { # I'm unsure what these values are for exactly, but they don't appear to impact bot functionality in any way.
1311
- :'$os' => RUBY_PLATFORM.to_s,
1312
- :'$browser' => 'discordrb',
1313
- :'$device' => 'discordrb',
1314
- :'$referrer' => '',
1315
- :'$referring_domain' => ''
1316
- },
1317
- large_threshold: large_threshold,
1318
- compress: true
1319
- }
1320
- }
1321
-
1322
- # Discord is very strict about the existence of the shard parameter, so only add it if it actually exists
1323
- packet[:d][:shard] = @shard_key if @shard_key
1324
-
1325
- @ws.send(packet.to_json)
1326
- end
1327
-
1328
- # Resume a previous gateway connection when reconnecting to a different server
1329
- def resume(seq, token, session_id)
1330
- data = {
1331
- op: Opcodes::RESUME,
1332
- d: {
1333
- seq: seq,
1334
- token: token,
1335
- session_id: session_id
1336
- }
1337
- }
1338
-
1339
- @ws.send(data.to_json)
1340
- end
1341
-
1342
- # Invalidate the current session (whatever this means)
1343
- def invalidate_session
1344
- @sequence = 0
1345
- @session_id = nil
1346
- end
1347
-
1348
1068
  # Notifies everything there is to be notified that the connection is now ready
1349
1069
  def notify_ready
1350
1070
  # Make sure to raise the event
1351
1071
  raise_event(ReadyEvent.new(self))
1352
1072
  LOGGER.good 'Ready'
1353
1073
 
1354
- # Tell the run method that everything was successful
1355
- @ws_success = true
1356
- end
1357
-
1358
- # Separate method to wait an ever-increasing amount of time before reconnecting after being disconnected in an
1359
- # unexpected way
1360
- def wait_for_reconnect
1361
- # We disconnected in an unexpected way! Wait before reconnecting so we don't spam Discord's servers.
1362
- debug("Attempting to reconnect in #{@falloff} seconds.")
1363
- sleep @falloff
1364
-
1365
- # Calculate new falloff
1366
- @falloff *= 1.5
1367
- @falloff = 115 + (rand * 10) if @falloff > 120 # Cap the falloff at 120 seconds and then add some random jitter
1368
- end
1369
-
1370
- def send_heartbeat(sequence = nil)
1371
- sequence ||= @sequence
1372
-
1373
- raise_event(HeartbeatEvent.new(self))
1374
-
1375
- LOGGER.out("Sending heartbeat with sequence #{sequence}")
1376
- data = {
1377
- op: Opcodes::HEARTBEAT,
1378
- d: sequence
1379
- }
1380
-
1381
- @ws.send(data.to_json)
1382
- rescue => e
1383
- LOGGER.error('Got an error while sending a heartbeat! Carrying on anyway because heartbeats are vital for the connection to stay alive')
1384
- LOGGER.log_exception(e)
1074
+ @gateway.notify_ready
1385
1075
  end
1386
1076
 
1387
1077
  def raise_event(event)