Class: Discordrb::Gateway

Inherits:
Object
  • Object
show all
Defined in:
lib/discordrb/gateway.rb

Overview

Client for the Discord gateway protocol

Constant Summary collapse

LARGE_THRESHOLD =

How many members there need to be in a server for it to count as "large"

100
GATEWAY_VERSION =

The version of the gateway that's supposed to be used.

9
FATAL_CLOSE_CODES =

Close codes that are unrecoverable, after which we should not try to reconnect.

  • 4003: Not authenticated. How did this happen?
  • 4004: Authentication failed. Token was wrong, nothing we can do.
  • 4011: Sharding required. Currently requires developer intervention.
  • 4014: Use of disabled privileged intents.
[4003, 4004, 4011, 4014].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = ALL_INTENTS) ⇒ Gateway

Returns a new instance of Gateway.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/discordrb/gateway.rb', line 165

def initialize(bot, token, shard_key = nil, compress_mode = :stream, intents = ALL_INTENTS)
  @token = token
  @bot = bot

  @shard_key = shard_key

  # Whether the connection to the gateway has succeeded yet
  @ws_success = false

  @check_heartbeat_acks = true

  @compress_mode = compress_mode
  @intents = intents
end

Instance Attribute Details

#check_heartbeat_ackstrue, false

Heartbeat ACKs are Discord's way of verifying on the client side whether the connection is still alive. If this is set to true (default value) the gateway client will use that functionality to detect zombie connections and reconnect in such a case; however it may lead to instability if there's some problem with the ACKs. If this occurs it can simply be set to false.

Returns:

  • (true, false)

    whether or not this gateway should check for heartbeat ACKs.



160
161
162
# File 'lib/discordrb/gateway.rb', line 160

def check_heartbeat_acks
  @check_heartbeat_acks
end

#intentsInteger (readonly)

Returns the intent parameter sent to the gateway server.

Returns:

  • (Integer)

    the intent parameter sent to the gateway server.



163
164
165
# File 'lib/discordrb/gateway.rb', line 163

def intents
  @intents
end

Instance Method Details

#heartbeatObject

Sends a heartbeat with the last received packet's seq (to acknowledge that we have received it and all packets before it), or if none have been received yet, with 0.

See Also:



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/discordrb/gateway.rb', line 268

def heartbeat
  if check_heartbeat_acks
    unless @last_heartbeat_acked
      # We're in a bad situation - apparently the last heartbeat wasn't ACK'd, which means the connection is likely
      # a zombie. Reconnect
      LOGGER.warn('Last heartbeat was not acked, so this is a zombie connection! Reconnecting')

      # We can't send anything on zombie connections
      @pipe_broken = true
      reconnect
      return
    end

    @last_heartbeat_acked = false
  end

  send_heartbeat(@session ? @session.sequence : 0)
end

#identifyObject

Identifies to Discord with the default parameters.

See Also:



296
297
298
299
300
301
302
303
304
305
# File 'lib/discordrb/gateway.rb', line 296

def identify
  compress = @compress_mode == :large
  send_identify(@token, {
                  os: RUBY_PLATFORM,
                  browser: 'discordrb',
                  device: 'discordrb',
                  referrer: '',
                  referring_domain: ''
                }, compress, LARGE_THRESHOLD, @shard_key, @intents)
end

#inject_error(e) ⇒ Object

Injects a terminal gateway error into the handler. Useful for testing the reconnect logic.

Parameters:

  • e (Exception)

    The exception object to inject.



261
262
263
# File 'lib/discordrb/gateway.rb', line 261

def inject_error(e)
  handle_internal_close(e)
end

#inject_reconnect(url = nil) ⇒ Object

Injects a reconnect event (op 7) into the event processor, causing Discord to reconnect to the given gateway URL. If the URL is set to nil, it will reconnect and get an entirely new gateway URL. This method has not much use outside of testing and implementing highly custom reconnect logic.

Parameters:

  • url (String, nil) (defaults to: nil)

    the URL to connect to or nil if one should be obtained from Discord.



241
242
243
244
245
246
247
248
249
# File 'lib/discordrb/gateway.rb', line 241

def inject_reconnect(url = nil)
  # When no URL is specified, the data should be nil, as is the case with Discord-sent packets.
  data = url ? { url: url } : nil

  handle_message({
    op: Opcodes::RECONNECT,
    d: data
  }.to_json)
end

#inject_resume(seq) ⇒ Object

Injects a resume packet (op 6) into the gateway. If this is done with a running connection, it will cause an error. It has no use outside of testing stuff that I know of, but if you want to use it anyway for some reason, here it is.

Parameters:

  • seq (Integer, nil)

    The sequence ID to inject, or nil if the currently tracked one should be used.



255
256
257
# File 'lib/discordrb/gateway.rb', line 255

def inject_resume(seq)
  send_resume(raw_token, @session_id, seq || @sequence)
end

#killObject

Kills the websocket thread, stopping all connections to Discord.



227
228
229
# File 'lib/discordrb/gateway.rb', line 227

def kill
  @ws_thread.kill
end

#notify_readyObject

Notifies the #run_async method that everything is ready and the caller can now continue (i.e. with syncing, or with doing processing and then syncing)



233
234
235
# File 'lib/discordrb/gateway.rb', line 233

def notify_ready
  @ws_success = true
end

#open?Boolean

Whether the WebSocket connection to the gateway is currently open

Returns:

  • (Boolean)


210
211
212
# File 'lib/discordrb/gateway.rb', line 210

def open?
  @handshake&.finished? && !@closed
end

#reconnect(attempt_resume = true) ⇒ Object

Reconnects the gateway connection in a controlled manner.

Parameters:

  • attempt_resume (true, false) (defaults to: true)

    Whether a resume should be attempted after the reconnection.



385
386
387
388
389
390
391
392
# File 'lib/discordrb/gateway.rb', line 385

def reconnect(attempt_resume = true)
  @session.suspend if @session && attempt_resume

  @instant_reconnect = true
  @should_reconnect = true

  close(4000)
end

#resumeObject

Resumes the session from the last recorded point.

See Also:



379
380
381
# File 'lib/discordrb/gateway.rb', line 379

def resume
  send_resume(@token, @session.session_id, @session.sequence)
end

#run_asyncObject

Connect to the gateway server in a separate thread



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/discordrb/gateway.rb', line 181

def run_async
  @ws_thread = Thread.new do
    Thread.current[:discordrb_name] = 'websocket'
    connect_loop
    LOGGER.warn('The WS loop exited! Not sure if this is a good thing')
  end

  LOGGER.debug('WS thread created! Now waiting for confirmation that everything worked')
  loop do
    sleep(0.5)

    if @ws_success
      LOGGER.debug('Confirmation received! Exiting run.')
      break
    end

    if @should_reconnect == false
      LOGGER.debug('Reconnection flag was unset. Exiting run.')
      break
    end
  end
end

#send_heartbeat(sequence) ⇒ Object

Sends a heartbeat packet (op 1). This tells Discord that the current connection is still active and that the last packets until the given sequence have been processed (in case of a resume).

Parameters:

  • sequence (Integer)

    The sequence number for which to send a heartbeat.



290
291
292
# File 'lib/discordrb/gateway.rb', line 290

def send_heartbeat(sequence)
  send_packet(Opcodes::HEARTBEAT, sequence)
end

#send_identify(token, properties, compress, large_threshold, shard_key = nil, intents = ALL_INTENTS) ⇒ Object

Sends an identify packet (op 2). This starts a new session on the current connection and tells Discord who we are. This can only be done once a connection.

Parameters:

  • token (String)

    The token with which to authorise the session. If it belongs to a bot account, it must be prefixed with "Bot ".

  • properties (Hash<Symbol => String>)

    A list of properties for Discord to use in analytics. The following keys are recognised:

    • "os" (recommended value: the operating system the bot is running on)
    • "browser" (recommended value: library name)
    • "device" (recommended value: library name)
    • "referrer" (recommended value: empty)
    • "referring_domain" (recommended value: empty)
  • compress (true, false)

    Whether certain large packets should be compressed using zlib.

  • large_threshold (Integer)

    The member threshold after which a server counts as large and will have to have its member list chunked.

  • shard_key (Array(Integer, Integer), nil) (defaults to: nil)

    The shard key to use for sharding, represented as [shard_id, num_shards], or nil if the bot should not be sharded.



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/discordrb/gateway.rb', line 325

def send_identify(token, properties, compress, large_threshold, shard_key = nil, intents = ALL_INTENTS)
  data = {
    # Don't send a v anymore as it's entirely determined by the URL now
    token: token,
    properties: properties,
    compress: compress,
    large_threshold: large_threshold,
    intents: intents
  }

  # Don't include the shard key at all if it is nil as Discord checks for its mere existence
  data[:shard] = shard_key if shard_key

  send_packet(Opcodes::IDENTIFY, data)
end

#send_packet(opcode, packet) ⇒ Object

Sends a custom packet over the connection. This can be useful to implement future yet unimplemented functionality or for testing. You probably shouldn't use this unless you know what you're doing.

Parameters:

  • opcode (Integer)

    The opcode the packet should be sent as. Can be one of Opcodes or a custom value if necessary.

  • packet (Object)

    Some arbitrary JSON-serializable data that should be sent as the d field.



436
437
438
439
440
441
442
443
# File 'lib/discordrb/gateway.rb', line 436

def send_packet(opcode, packet)
  data = {
    op: opcode,
    d: packet
  }

  send(data.to_json)
end

#send_raw(data, type = :text) ⇒ Object

Sends custom raw data over the connection. Only useful for testing; even if you know what you're doing you probably want to use #send_packet instead.

Parameters:

  • data (String)

    The data to send.

  • type (Symbol) (defaults to: :text)

    The type the WebSocket frame should have; either :text, :binary, :ping, :pong, or :close.



450
451
452
# File 'lib/discordrb/gateway.rb', line 450

def send_raw(data, type = :text)
  send(data, type)
end

#send_request_members(server_id, query, limit) ⇒ Object

Sends a request members packet (op 8). This will order Discord to gradually sent all requested members as dispatch events with type GUILD_MEMBERS_CHUNK. It is necessary to use this method in order to get all members of a large server (see large_threshold in #send_identify), however it can also be used for other purposes.

Parameters:

  • server_id (Integer)

    The ID of the server whose members to query.

  • query (String)

    If this string is not empty, only members whose username starts with this string will be returned.

  • limit (Integer)

    How many members to send at maximum, or 0 to send all members.



421
422
423
424
425
426
427
428
429
# File 'lib/discordrb/gateway.rb', line 421

def send_request_members(server_id, query, limit)
  data = {
    guild_id: server_id,
    query: query,
    limit: limit
  }

  send_packet(Opcodes::REQUEST_MEMBERS, data)
end

#send_resume(token, session_id, seq) ⇒ Object

Sends a resume packet (op 6). This replays all events from a previous point specified by its packet sequence. This will not work if the packet to resume from has already been acknowledged using a heartbeat, or if the session ID belongs to a now invalid session.

If this packet is sent at the beginning of a connection, it will act similarly to an #identify in that it creates a session on the current connection. Unlike identify however, this packet can also be sent in an existing session and will just replay some of the events.

Parameters:

  • token (String)

    The token that was used to identify the session to resume.

  • session_id (String)

    The session ID of the session to resume.

  • seq (Integer)

    The packet sequence of the packet after which the events should be replayed.



404
405
406
407
408
409
410
411
412
# File 'lib/discordrb/gateway.rb', line 404

def send_resume(token, session_id, seq)
  data = {
    token: token,
    session_id: session_id,
    seq: seq
  }

  send_packet(Opcodes::RESUME, data)
end

#send_status_update(status, since, game, afk) ⇒ Object

Sends a status update packet (op 3). This sets the bot user's status (online/idle/...) and game playing/streaming.

Parameters:

  • status (String)

    The status that should be set (online, idle, dnd, invisible).

  • since (Integer)

    The Unix timestamp in milliseconds when the status was set. Should only be provided when afk is true.

  • game (Hash<Symbol => Object>, nil)

    nil if no game should be played, or a hash of :game => "name" if a game should be played. The hash can also contain additional attributes for streaming statuses.

  • afk (true, false)

    Whether the status was set due to inactivity on the user's part.



348
349
350
351
352
353
354
355
356
357
# File 'lib/discordrb/gateway.rb', line 348

def send_status_update(status, since, game, afk)
  data = {
    status: status,
    since: since,
    game: game,
    afk: afk
  }

  send_packet(Opcodes::PRESENCE, data)
end

#send_voice_state_update(server_id, channel_id, self_mute, self_deaf) ⇒ Object

Sends a voice state update packet (op 4). This packet can connect a user to a voice channel, update self mute/deaf status in an existing voice connection, move the user to a new voice channel on the same server or disconnect an existing voice connection.

Parameters:

  • server_id (Integer)

    The ID of the server on which this action should occur.

  • channel_id (Integer, nil)

    The channel ID to connect/move to, or nil to disconnect.

  • self_mute (true, false)

    Whether the user should itself be muted to everyone else.

  • self_deaf (true, false)

    Whether the user should be deaf towards other users.



366
367
368
369
370
371
372
373
374
375
# File 'lib/discordrb/gateway.rb', line 366

def send_voice_state_update(server_id, channel_id, self_mute, self_deaf)
  data = {
    guild_id: server_id,
    channel_id: channel_id,
    self_mute: self_mute,
    self_deaf: self_deaf
  }

  send_packet(Opcodes::VOICE_STATE, data)
end

#stopObject

Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that Discord is immediately aware of the closed connection and makes the bot appear offline instantly.

If this method doesn't work or you're looking for something more drastic, use #kill instead.



218
219
220
221
222
223
224
# File 'lib/discordrb/gateway.rb', line 218

def stop
  @should_reconnect = false
  close

  # Return nil so command bots don't send a message
  nil
end

#syncObject

Prevents all further execution until the websocket thread stops (e.g. through a closed connection).



205
206
207
# File 'lib/discordrb/gateway.rb', line 205

def sync
  @ws_thread.join
end