class Discordrb::Voice::VoiceWS

Represents a websocket client connection to the voice server. The websocket connection (sometimes called vWS) is used to manage general data about the connection, such as sending the speaking packet, which determines the green circle around users on Discord, and obtaining UDP connection info.

Constants

VOICE_GATEWAY_VERSION

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

Attributes

udp[R]

@return [VoiceUDP] the UDP voice connection over which the actual audio data is sent.

Public Class Methods

new(channel, bot, token, session, endpoint) click to toggle source

Makes a new voice websocket client, but doesn’t connect it (see {#connect} for that) @param channel [Channel] The voice channel to connect to @param bot [Bot] The regular bot to which this vWS is bound @param token [String] The authentication token which is also used for REST requests @param session [String] The voice session ID Discord sends over the regular websocket @param endpoint [String] The endpoint URL to connect to

# File lib/discordrb/voice/network.rb, line 181
def initialize(channel, bot, token, session, endpoint)
  raise 'libsodium is unavailable - unable to create voice bot! Please read https://github.com/shardlab/discordrb/wiki/Installing-libsodium' unless LIBSODIUM_AVAILABLE

  @channel = channel
  @bot = bot
  @token = token
  @session = session

  @endpoint = endpoint.split(':').first

  @udp = VoiceUDP.new
end

Public Instance Methods

connect() click to toggle source

Communication goes like this: me discord

|                      |

websocket connect -> |

|                      |
|     <- websocket opcode 2
|                      |

UDP discovery -> |

|                      |
|       <- UDP reply packet
|                      |

websocket opcode 1 -> |

|                      |

# File lib/discordrb/voice/network.rb, line 307
def connect
  # Connect websocket
  @thread = Thread.new do
    Thread.current[:discordrb_name] = 'vws'
    init_ws
  end

  @bot.debug('Started websocket initialization, now waiting for UDP discovery reply')

  # Now wait for opcode 2 and the resulting UDP reply packet
  ip, port = @udp.receive_discovery_reply
  @bot.debug("UDP discovery reply received! #{ip} #{port}")

  # Send UDP init packet with received UDP data
  send_udp_connection(ip, port, @udp_mode)

  @bot.debug('Waiting for op 4 now')

  # Wait for op 4, then finish
  sleep 0.05 until @ready
end
destroy() click to toggle source

Disconnects the websocket and kills the thread

# File lib/discordrb/voice/network.rb, line 330
def destroy
  @heartbeat_running = false
end
send_heartbeat() click to toggle source

Send a heartbeat (op 3), has to be done every @heartbeat_interval seconds or the connection will terminate

# File lib/discordrb/voice/network.rb, line 230
def send_heartbeat
  millis = Time.now.strftime('%s%L').to_i
  @bot.debug("Sending voice heartbeat at #{millis}")

  @client.send({
    op: 3,
    d: millis
  }.to_json)
end
send_init(server_id, bot_user_id, session_id, token) click to toggle source

Send a connection init packet (op 0) @param server_id [Integer] The ID of the server to connect to @param bot_user_id [Integer] The ID of the bot that is connecting @param session_id [String] The voice session ID @param token [String] The Discord authentication token

# File lib/discordrb/voice/network.rb, line 199
def send_init(server_id, bot_user_id, session_id, token)
  @client.send({
    op: 0,
    d: {
      server_id: server_id,
      user_id: bot_user_id,
      session_id: session_id,
      token: token
    }
  }.to_json)
end
send_speaking(value) click to toggle source

Send a speaking packet (op 5). This determines the green circle around the avatar in the voice channel @param value [true, false, Integer] Whether or not the bot should be speaking, can also be a bitmask denoting audio type.

# File lib/discordrb/voice/network.rb, line 242
def send_speaking(value)
  @bot.debug("Speaking: #{value}")
  @client.send({
    op: 5,
    d: {
      speaking: value,
      delay: 0
    }
  }.to_json)
end
send_udp_connection(ip, port, mode) click to toggle source

Sends the UDP connection packet (op 1) @param ip [String] The IP to bind UDP to @param port [Integer] The port to bind UDP to @param mode [Object] Which mode to use for the voice connection

# File lib/discordrb/voice/network.rb, line 215
def send_udp_connection(ip, port, mode)
  @client.send({
    op: 1,
    d: {
      protocol: 'udp',
      data: {
        address: ip,
        port: port,
        mode: mode
      }
    }
  }.to_json)
end
websocket_message(msg) click to toggle source

@!visibility private

# File lib/discordrb/voice/network.rb, line 264
def websocket_message(msg)
  @bot.debug("Received VWS message! #{msg}")
  packet = JSON.parse(msg)

  case packet['op']
  when 2
    # Opcode 2 contains data to initialize the UDP connection
    @ws_data = packet['d']

    @ssrc = @ws_data['ssrc']
    @port = @ws_data['port']

    @udp_mode = (ENCRYPTION_MODES & @ws_data['modes']).first

    @udp.connect(@ws_data['ip'], @port, @ssrc)
    @udp.send_discovery
  when 4
    # Opcode 4 sends the secret key used for encryption
    @ws_data = packet['d']

    @ready = true
    @udp.secret_key = @ws_data['secret_key'].pack('C*')
    @udp.mode = @ws_data['mode']
  when 8
    # Opcode 8 contains the heartbeat interval.
    @heartbeat_interval = packet['d']['heartbeat_interval']
  end
end
websocket_open() click to toggle source

Event handlers; public for websocket-simple to work correctly @!visibility private

# File lib/discordrb/voice/network.rb, line 255
def websocket_open
  # Give the current thread a name ('Voice Web Socket Internal')
  Thread.current[:discordrb_name] = 'vws-i'

  # Send the init packet
  send_init(@channel.server.id, @bot.profile.id, @session, @token)
end

Private Instance Methods

heartbeat_loop() click to toggle source
# File lib/discordrb/voice/network.rb, line 336
def heartbeat_loop
  @heartbeat_running = true
  while @heartbeat_running
    if @heartbeat_interval
      sleep @heartbeat_interval / 1000.0
      send_heartbeat
    else
      # If no interval has been set yet, sleep a second and check again
      sleep 1
    end
  end
end
init_ws() click to toggle source
# File lib/discordrb/voice/network.rb, line 349
def init_ws
  host = "wss://#{@endpoint}:443/?v=#{VOICE_GATEWAY_VERSION}"
  @bot.debug("Connecting VWS to host: #{host}")

  # Connect the WS
  @client = Discordrb::WebSocket.new(
    host,
    method(:websocket_open),
    method(:websocket_message),
    proc { |e| Discordrb::LOGGER.error "VWS error: #{e}" },
    proc { |e| Discordrb::LOGGER.warn "VWS close: #{e}" }
  )

  @bot.debug('VWS connected')

  # Block any further execution
  heartbeat_loop
end