vox 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2849016a778799a4bf2b6b575410b2c8b1283676f017bbdccd6f4e8ae152e587
4
- data.tar.gz: 38b16cc95ed5eb7a72bbc878db3f5c52a1db13f658562ba7a596f86dc4070a22
3
+ metadata.gz: ff24b453b9f1e77745b7cf69fc4e37b2c8b41ea878fd25bb4d90a1ddff53cb84
4
+ data.tar.gz: 7eb6a27c3b74b29659689cee45eec1b5754262893103b6e4e1feac27899128f2
5
5
  SHA512:
6
- metadata.gz: 1197ad4ce07851d592a3a723798bf660965e0dfd8857ec90d90267e5f1d6e7702be543b2c37cdea240f9f3016d7869e4d02b38cb96b7c8ca9e71f6205170bb95
7
- data.tar.gz: 83750ec950b0bcfca2e4ca91d74d12a8232230e53ec943c8fbab839fcd76c0525f684f1bd2e206d360c8e354c2bb04e5e180adadb5323992dad5a7c27adcbc4e
6
+ metadata.gz: 49c6cd9c4c7b512a9937b918facf227131ee5bf473bc8b69b5098314abd42796eaa16a2502d8e0255dfecc85c0a362e479d3c8642c1edac1910ab0a0bcf834b8
7
+ data.tar.gz: 2b1c08120d3599f5985d99c0efb0d35658ebd23ca163a07ad33e15e7a9165e9ab842bcd4f704c4a82823af97db0f28ad471c2a6ce77a038eaa58a4f61f0ec976
@@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.2.2] - 2020-9-13
8
+ ### Added
9
+ - Vox.setup_default_logger
10
+ - Gateway component
11
+ - Gateway routes (get_gateway, get_gateway_bot)
8
12
 
9
13
  ## [0.2.1] - 2020-8-29
10
14
  ### Added
data/Rakefile CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
  require 'rubocop/rake_task'
6
+ require 'yard'
7
+ require 'yard/rake/yardoc_task'
6
8
 
7
9
  RSpec::Core::RakeTask.new(:spec) do |t|
8
10
  t.rspec_opts = '--format progress'
@@ -11,4 +13,9 @@ end
11
13
 
12
14
  RuboCop::RakeTask.new(:rubocop)
13
15
 
16
+ YARD::Rake::YardocTask.new(:yard)
17
+
18
+ # Run all tests
19
+ task test: %i[rubocop yard spec]
20
+
14
21
  task default: :spec
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vox/http/client'
4
+ require 'vox/gateway/client'
5
+
6
+ Vox.setup_default_logger
7
+
8
+ token = ENV['VOX_TOKEN']
9
+ rest = Vox::HTTP::Client.new(token)
10
+ gateway = Vox::Gateway::Client.new(url: rest.get_gateway[:url], token: token)
11
+
12
+ gateway.on(:MESSAGE_CREATE) do |data|
13
+ rest.create_message(data[:channel_id], content: 'pong') if data[:content] == 'vox.ping'
14
+ end
15
+
16
+ Signal.trap('INT') { gateway.close('Disconnecting') }
17
+
18
+ gateway.connect
data/lib/vox.rb CHANGED
@@ -4,6 +4,33 @@ require 'vox/version'
4
4
 
5
5
  # Parent module containing all component pieces
6
6
  module Vox
7
+ # Setup default appenders, log level, and formatting scheme.
8
+ # @param root_level [Symbol] The default logging level for all `Vox` loggers.
9
+ # @param rules [Hash<Class, Symbol>] Custom levels for each desired class.
10
+ # @example
11
+ # Vox.setup_default_logger(root_level: :warn, Vox::HTTP: :info, Vox::Gateway: :info)
12
+ def self.setup_default_logger(root_level: :info, **rules)
13
+ Logging.logger[Vox].level = root_level
14
+
15
+ rules.each do |log, level|
16
+ Logging.logger[log].level = level
17
+ end
18
+
19
+ Logging.color_scheme('vox_default',
20
+ levels: {
21
+ debug: :magenta,
22
+ info: :green,
23
+ warn: :yellow,
24
+ error: :red,
25
+ fatal: %i[white on_red]
26
+ },
27
+ date: :blue,
28
+ logger: :cyan)
29
+
30
+ Logging.logger[Vox].add_appenders(
31
+ Logging.appenders.stdout(layout: Logging.layouts.pattern(color_scheme: 'vox_default'))
32
+ )
33
+ end
7
34
  # Catch all error for all Vox error subclasses
8
35
  class Error < StandardError; end
9
36
  # Your code goes here...
@@ -0,0 +1,376 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vox/gateway/websocket'
4
+ require 'logging'
5
+
6
+ module Vox
7
+ module Gateway
8
+ # A client for receiving and writing data from the gateway.
9
+ # The client uses an emitter pattern for emitting and registering events.
10
+ # @example
11
+ # client.on(:MESSAGE_CREATE) do |payload|
12
+ # puts "Hello!" if payload[:content] == "hello"
13
+ # end
14
+ class Client
15
+ include EventEmitter
16
+
17
+ # @!visibility private
18
+ # The default properties for the identify packet
19
+ DEFAULT_PROPERTIES = {
20
+ '$os': Gem::Platform.local.os,
21
+ '$browser': 'vox',
22
+ '$device': 'vox'
23
+ }.freeze
24
+
25
+ # @!visibility private
26
+ # A hash of opcodes => op_names, as well as op_names => opcodes.
27
+ OPCODES = {
28
+ 0 => :DISPATCH,
29
+ 1 => :HEARTBEAT,
30
+ 2 => :IDENTIFY,
31
+ 3 => :PRESENCE_UPDATE,
32
+ 4 => :VOICE_STATE_UPDATE,
33
+ 5 => :UNKNOWN,
34
+ 6 => :RESUME,
35
+ 7 => :RECONNECT,
36
+ 8 => :REQUEST_GUILD_MEMBERS,
37
+ 9 => :INVALID_SESSION,
38
+ 10 => :HELLO,
39
+ 11 => :HEARTBEAT_ACK
40
+ }.tap { |ops| ops.merge!(ops.invert) }.freeze
41
+
42
+ # The gateway version to request.
43
+ GATEWAY_VERSION = '8'
44
+
45
+ # Class that holds information about a session.
46
+ Session = Struct.new(:id, :seq)
47
+
48
+ # @return [Session] The connection's session information.
49
+ attr_reader :session
50
+
51
+ # @param url [String] The url to use when connecting to the websocket. This can be
52
+ # retrieved from the API with {HTTP::Routes::Gateway#get_gateway_bot}.
53
+ # @param token [String] The token to use for authorization.
54
+ # @param port [Integer] The port to use when connecting. If `nil`, it will be inferred
55
+ # from the URL scheme (80 for `ws`, and 443 for `wss`).
56
+ # @param encoding [:json] This only accepts `json` currently, but may support `:etf` in future versions.
57
+ # @param compress [true, false] Whether to use `zlib-stream` compression.
58
+ # @param shard [Array<Integer>] An array in the format `[ShardNumber, TotalShards]`.
59
+ # @param large_threshold [Integer]
60
+ # @param presence [Object]
61
+ # @param intents [Integer]
62
+ def initialize(url:, token:, port: nil, encoding: :json, compress: true, shard: [0, 1],
63
+ properties: DEFAULT_PROPERTIES, large_threshold: nil, presence: nil, intents: nil)
64
+ uri = create_gateway_uri(url, port: port, encoding: encoding, compress: compress)
65
+
66
+ @encoding = encoding
67
+ raise ArgumentError, 'Invalid gateway encoding' unless %i[json etf].include? @encoding
68
+
69
+ if @encoding == :etf
70
+ begin
71
+ require 'vox/etf'
72
+ rescue LoadError
73
+ Logging.logger[self].error { 'ETF parsing lib not found. Please install vox-etf to use ETF encoding.' }
74
+ raise Vox::Error.new('ETF lib not found')
75
+ end
76
+ end
77
+
78
+ @websocket = WebSocket.new(uri.to_s, port: uri.port, compression: compress)
79
+ @identify_opts = {
80
+ token: token, properties: properties, shard: shard,
81
+ large_threshold: large_threshold, presence: presence, intents: intents
82
+ }.compact
83
+ @session = Session.new
84
+ @should_reconnect = Queue.new
85
+ setup_handlers
86
+ end
87
+
88
+ # @!method on(event, &block)
89
+ # Register an event handler for a GATEWAY event, or DISPATCH event.
90
+ # When registering an event corresponding to an opcode, the full payload
91
+ # is yielded. When registering a DISPATCH type, only the data portion
92
+ # of the payload is provided.
93
+
94
+ # Connect the websocket to the gateway.
95
+ def connect(async: false)
96
+ @ws_thread = Thread.new do
97
+ loop do
98
+ @websocket.connect
99
+ @websocket.thread.join
100
+ break unless @should_reconnect.shift
101
+ end
102
+ end
103
+ async ? @ws_thread : @ws_thread.join
104
+ end
105
+
106
+ # Close the websocket.
107
+ # @param code [Integer] The close code.
108
+ # @param reason [String] The reason for closing.
109
+ def close(reason = nil, code = 1000, reconnect: false)
110
+ @ws_thread.kill unless reconnect
111
+ @websocket.close(reason, code)
112
+ @websocket.thread.join unless reconnect
113
+ end
114
+
115
+ # Send a packet with the correct encoding. Only supports JSON currently.
116
+ # @param op_code [Integer]
117
+ # @param data [Hash]
118
+ def send_packet(op_code, data)
119
+ LOGGER.debug { "Sending #{op_code.is_a?(Symbol) ? op_code : OPCODES[op_code]} #{data || 'nil'}" }
120
+ if @encoding == :etf
121
+ send_etf_packet(op_code, data)
122
+ else
123
+ send_json_packet(op_code, data)
124
+ end
125
+ end
126
+
127
+ # Request a guild member chunk, used to build a member cache.
128
+ # @param guild_id [String, Integer]
129
+ # @param query
130
+ # @param limit [Integer]
131
+ # @param presences
132
+ # @param user_ids [Array<String, Integer>]
133
+ # @param nonce [String, Integer]
134
+ def request_guild_members(guild_id, query: nil, limit: 0, presences: nil,
135
+ user_ids: nil, nonce: nil)
136
+ opts = {
137
+ guild_id: guild_id, query: query, limit: limit, presences: presences,
138
+ user_ids: user_ids, nonce: nonce
139
+ }.compact
140
+
141
+ send_packet(OPCODES[:REQUEST_GUILD_MEMBERS], opts)
142
+ end
143
+
144
+ # Send a voice state update, used for establishing voice connections.
145
+ # @param guild_id [String, Integer]
146
+ # @param channel_id [String, Integer]
147
+ # @param self_mute [true, false]
148
+ # @param self_deaf [true, false]
149
+ def voice_state_update(guild_id, channel_id, self_mute: false, self_deaf: false)
150
+ opts = {
151
+ guild_id: guild_id, channel_id: channel_id, self_mute: self_mute,
152
+ self_deaf: self_deaf
153
+ }.compact
154
+
155
+ send_packet(OPCODES[:VOICE_STATE_UPDATE], opts)
156
+ end
157
+
158
+ # Update the bot's status.
159
+ # @param status [String] The user's new status.
160
+ # @param afk [true, false] Whether or not the client is AFK.
161
+ # @param game [Hash<Symbol, Object>, nil] An [activity object](https://discord.com/developers/docs/topics/gateway#activity-object).
162
+ # @param since [Integer, nil] Unix time (in milliseconds) of when the client went idle.
163
+ def presence_update(status:, afk: false, game: nil, since: nil)
164
+ opts = { status: status, afk: afk, game: game, since: since }.compact
165
+ send_packet(OPCODES[:PRESENCE_UPDATE], opts)
166
+ end
167
+
168
+ private
169
+
170
+ # Add internal event handlers
171
+ def setup_handlers
172
+ # Discord will contact us with HELLO first, so we don't need to hook into READY
173
+ @websocket.on(:message, &method(:handle_message))
174
+ @websocket.on(:close, &method(:handle_close))
175
+
176
+ # Setup payload handlers
177
+ on(:DISPATCH, &method(:handle_dispatch))
178
+ on(:HEARTBEAT, &method(:handle_heartbeat))
179
+ on(:RECONNECT, &method(:handle_reconnect))
180
+ on(:INVALID_SESSION, &method(:handle_invalid_session))
181
+ on(:HELLO, &method(:handle_hello))
182
+ on(:HEARTBEAT_ACK, &method(:handle_heartbeat_ack))
183
+ on(:READY, &method(:handle_ready))
184
+ end
185
+
186
+ # Create a URI from a gateway url and options
187
+ # @param url [String]
188
+ # @param port [Integer]
189
+ # @param encoding [:json]
190
+ # @param compress [true, false]
191
+ # @return [URI::Generic]
192
+ def create_gateway_uri(url, port: nil, encoding: :json, compress: true)
193
+ compression = compress ? 'zlib-stream' : nil
194
+ query = URI.encode_www_form(
195
+ version: GATEWAY_VERSION, encoding: encoding, compress: compression
196
+ )
197
+ URI(url).tap do |u|
198
+ u.query = query
199
+ u.port = port
200
+ end
201
+ end
202
+
203
+ # Send a JSON packet.
204
+ # @param op_code [Integer]
205
+ # @param data [Hash]
206
+ def send_json_packet(op_code, data)
207
+ payload = { op: op_code, d: data }
208
+
209
+ @websocket.send_json(payload)
210
+ end
211
+
212
+ # Send an ETF packet.
213
+ # @param op_code [Integer]
214
+ # @param data [Hash]
215
+ def send_etf_packet(op_code, data)
216
+ payload = { op: op_code, d: data }
217
+ @websocket.send_binary(Vox::ETF.encode(payload))
218
+ end
219
+
220
+ # Send an identify payload to discord, beginning a new session.
221
+ def send_identify
222
+ send_packet(OPCODES[:IDENTIFY], @identify_opts)
223
+ end
224
+
225
+ # Send a resume payload to discord, attempting to resume an existing
226
+ # session.
227
+ def send_resume
228
+ send_packet(OPCODES[:RESUME],
229
+ { token: @identify_opts[:token], session_id: @session.id, seq: @session.seq })
230
+ end
231
+
232
+ # Send a heartbeat.
233
+ def send_heartbeat
234
+ @heartbeat_acked = false
235
+ send_packet(OPCODES[:HEARTBEAT], @session.seq)
236
+ end
237
+
238
+ # A loop that handles sending and receiving heartbeats from the gateway.
239
+ def heartbeat_loop
240
+ loop do
241
+ send_heartbeat
242
+ sleep @heartbeat_interval
243
+ next if @heartbeat_acked
244
+
245
+ LOGGER.error { 'Heartbeat was not acked, reconnecting.' }
246
+ @websocket.close
247
+ break
248
+ end
249
+ end
250
+
251
+ ##################################
252
+ ##################################
253
+ ##################################
254
+ ##################################
255
+ #### ####
256
+ #### Internal event handlers ####
257
+ #### ####
258
+ ##################################
259
+ ##################################
260
+ ##################################
261
+ ##################################
262
+
263
+ # Handle a message from the websocket.
264
+ # @param data [String] The message data.
265
+ def handle_message(data)
266
+ if @encoding == :etf
267
+ handle_etf_message(data)
268
+ else
269
+ handle_json_message(data)
270
+ end
271
+ end
272
+
273
+ # Handle an ETF message, decoding it and emitting an event.
274
+ # @param data [String] The ETF data.
275
+ def handle_etf_message(data)
276
+ data = Vox::ETF.decode(data)
277
+ LOGGER.debug { "Emitting #{OPCODES[data[:op]]}" } if OPCODES[data[:op]] != :DISPATCH
278
+
279
+ @session.seq = data[:s] if data[:s]
280
+ op = OPCODES[data[:op]]
281
+
282
+ emit(op, data)
283
+ end
284
+
285
+ # Handle a JSON message, decoding it and emitting an event.
286
+ # @param json [String] The JSON data.
287
+ def handle_json_message(json)
288
+ data = MultiJson.load(json, symbolize_keys: true)
289
+ # Don't announce DISPATCH events since we log it on the same level
290
+ # in the dispatch handler.
291
+ LOGGER.debug { "Emitting #{OPCODES[data[:op]]}" } if OPCODES[data[:op]] != :DISPATCH
292
+
293
+ @session.seq = data[:s] if data[:s]
294
+ op = OPCODES[data[:op]]
295
+
296
+ emit(op, data)
297
+ end
298
+
299
+ # Handle a dispatch event, extracting the event name and emitting an event.
300
+ # @param payload [Hash<Symbol, Object>] The decoded payload's `data` field.
301
+ def handle_dispatch(payload)
302
+ LOGGER.debug { "Emitting #{payload[:t]}" }
303
+ emit(payload[:t], payload[:d])
304
+ end
305
+
306
+ # Handle a hello event, beginning the heartbeat loop and identifying or
307
+ # resuming.
308
+ # @param payload [Hash<Symbol, Object>] The decoded payload.
309
+ def handle_hello(payload)
310
+ LOGGER.info { 'Connected' }
311
+ @heartbeat_interval = payload[:d][:heartbeat_interval] / 1000
312
+ @heartbeat_thread = Thread.new { heartbeat_loop }
313
+ if @session.seq
314
+ send_resume
315
+ else
316
+ send_identify
317
+ end
318
+ end
319
+
320
+ # Fired if the gateway requests that we send a heartbeat.
321
+ # @param _payload [Object] The received payload, not used in this method.
322
+ def handle_heartbeat(_payload)
323
+ send_packet(OPCODES[:HEARTBEAT], @session.seq)
324
+ end
325
+
326
+ # Set session information from the ready payload.
327
+ # @param payload [Object] The received ready payload.
328
+ def handle_ready(payload)
329
+ @session.id = payload[:session_id]
330
+ end
331
+
332
+ # @param _payload [Object] The received payload, not used in this method.
333
+ def handle_invalid_session(_payload)
334
+ @session.seq = nil
335
+ send_identify
336
+ end
337
+
338
+ # @param _payload [Object] The received payload, not used in this method.
339
+ def handle_reconnect(_payload)
340
+ @websocket.close('Received reconnect', 4000)
341
+ end
342
+
343
+ # Handle a heartbeat acknowledgement from the gateway.
344
+ # @param _payload [Object] The received payload, not used in this method.
345
+ def handle_heartbeat_ack(_payload)
346
+ @heartbeat_acked = true
347
+ end
348
+
349
+ # Handle a close event from the websocket.
350
+ # @param data [Hash{:code => Integer, :reason => String}]
351
+ def handle_close(data)
352
+ LOGGER.warn { "Websocket closed (#{data[:code]} #{data[:reason]})" }
353
+ @heartbeat_thread&.kill
354
+ reconnect = true
355
+
356
+ case data[:code]
357
+ # Invalid seq when resuming, or session timed out
358
+ when 4007, 4009
359
+ LOGGER.error { 'Invalid session, reconnecting.' }
360
+ @session = Session.new
361
+ when 4003, 4004, 4011
362
+ LOGGER.fatal { data[:reason] }
363
+ reconnect = false
364
+ else
365
+ LOGGER.error { data[:reason] } if data[:reason]
366
+ end
367
+
368
+ @should_reconnect << reconnect
369
+ end
370
+
371
+ # @!visibility private
372
+ # The logger for Vox::Gateway::Client
373
+ LOGGER = Logging.logger[self]
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'event_emitter'
4
+ require 'logging'
5
+ require 'multi_json'
6
+ require 'websocket/driver'
7
+ require 'socket'
8
+ require 'openssl'
9
+ require 'uri'
10
+ require 'zlib'
11
+
12
+ module Vox
13
+ # Module containing the gateway component.
14
+ module Gateway
15
+ # Websocket that handles data interchange for {Vox::Gateway::Client}.
16
+ class WebSocket
17
+ include EventEmitter
18
+
19
+ # Zlib boundary used for separating messages split into multiple frames.
20
+ ZLIB_SUFFIX = "\x00\x00\xFF\xFF".b.freeze
21
+
22
+ attr_reader :url, :thread, :driver
23
+
24
+ def initialize(url, port: nil, compression: true)
25
+ @url = url
26
+ @uri = URI.parse(url)
27
+ @port = port || @uri.scheme == 'wss' ? 443 : 80
28
+ @inflate = Zlib::Inflate.new if compression
29
+ end
30
+
31
+ # @!method on(key, data)
32
+ # @overload on('open', &block)
33
+ # Emitted when the websocket finishes its connecting process.
34
+ # @overload on('message', &block)
35
+ # Emitted when a message is parsed from the websocket.
36
+ # @yieldparam [String] data The received message.
37
+ # @overload on('close', &block)
38
+ # Emitted when the websocket connection closes.
39
+ # @yieldparam [Integer] code The given close code.
40
+ # @yieldparam [String, nil] reason The reason the websocket was closed.
41
+
42
+ # Connect to the websocket server.
43
+ # @return [Thread] The thread handling the read loop.
44
+ def connect
45
+ # Flush the zlib buffer
46
+ @inflate&.reset
47
+
48
+ # Create a socket connection to the URL
49
+ @socket = create_socket
50
+
51
+ # Initialize the websocket driver
52
+ setup_driver
53
+
54
+ # Read until our websocket closes.
55
+ @thread = Thread.new do
56
+ read_loop
57
+ end
58
+ end
59
+
60
+ # Send a `text` message.
61
+ # @param message [String] The `text` message to write to the websocket.
62
+ # @return [true, false] Whether the message was sent successfully.
63
+ def send(message)
64
+ LOGGER.debug { "[OUT] #{message} " }
65
+ @driver.text(message)
66
+ end
67
+
68
+ # Serialize a hash to send as a `text` message.
69
+ # @param hash [Hash] The hash to serialize and send as a `text` message.
70
+ # @return [true, false] Whether the message was sent successfully.
71
+ def send_json(hash)
72
+ data = MultiJson.dump(hash)
73
+ send(data)
74
+ end
75
+
76
+ # Send a `binary` frame.
77
+ # @param data [String] The binary data to write to the websocket.
78
+ # @return [true, false] Whether the data was send successfully.
79
+ def send_binary(data)
80
+ @driver.binary(data)
81
+ end
82
+
83
+ # @!visibility private
84
+ # @param data [String] The data to write to the socket.
85
+ def write(data)
86
+ @socket.write(data)
87
+ end
88
+
89
+ # @!visibility private
90
+ # @return [nil]
91
+ def read
92
+ @driver.parse(@socket.readpartial(4096))
93
+ end
94
+
95
+ # Close the websocket connection.
96
+ # @param reason [String] The reason for closing the websocket.
97
+ # @param code [Integer] The code to close the websocket with.
98
+ # @return [true, false] Whether the websocket closed successfully.
99
+ def close(reason = nil, code = 1000)
100
+ @driver.close(reason, code)
101
+ end
102
+
103
+ private
104
+
105
+ # Read from the socket until the websocket driver
106
+ # reports as closed.
107
+ def read_loop
108
+ read until @driver.state == :closed
109
+ rescue SystemCallError => e
110
+ LOGGER.error { "(#{e.class.name.split('::').last}) #{e.message}" }
111
+ rescue EOFError => e
112
+ LOGGER.error { 'EOF in websocket loop' }
113
+ end
114
+
115
+ def setup_driver
116
+ @driver = ::WebSocket::Driver.client(self)
117
+ register_handlers
118
+ @driver.start
119
+ end
120
+
121
+ # Create a socket, create an SSL socket instead for
122
+ # wss.
123
+ # @return [TCPSocket, SSLSocket]
124
+ def create_socket
125
+ if @uri.scheme == 'wss'
126
+ create_ssl_socket.tap(&:connect)
127
+ else
128
+ TCPSocket.new(@uri.host, @port)
129
+ end
130
+ end
131
+
132
+ # Create an SSL socket for WSS.
133
+ def create_ssl_socket
134
+ ctx = OpenSSL::SSL::SSLContext.new
135
+ ctx.set_params ssl_version: :TLSv1_2
136
+
137
+ socket = TCPSocket.new(@uri.host, @port)
138
+ OpenSSL::SSL::SSLSocket.new(socket, ctx)
139
+ end
140
+
141
+ # Register the base handlers.
142
+ def register_handlers
143
+ @driver.on(:open, &method(:on_open))
144
+ @driver.on(:message, &method(:on_message))
145
+ @driver.on(:close, &method(:on_close))
146
+ end
147
+
148
+ # Handle open events.
149
+ def on_open(_event)
150
+ LOGGER.debug { 'Connection open' }
151
+ emit(:open)
152
+ end
153
+
154
+ # Handle parsed message events.
155
+ def on_message(event)
156
+ data = if @inflate
157
+ packed = event.data.pack('c*')
158
+ @inflate << packed
159
+ return unless packed.end_with?(ZLIB_SUFFIX)
160
+
161
+ @inflate.inflate('')
162
+ else
163
+ event.data
164
+ end
165
+
166
+ LOGGER.debug { "[IN] #{data[0].ord == 131 ? data.inspect : data}" }
167
+ emit(:message, data)
168
+ end
169
+
170
+ # Handle close events.
171
+ def on_close(event)
172
+ LOGGER.debug { "WebSocket is closing (#{event.code}) #{event.reason}" }
173
+ emit(:close, { code: event.code, reason: event.reason })
174
+ end
175
+
176
+ # The logger used for output.
177
+ LOGGER = Logging.logger[self]
178
+ end
179
+ end
180
+ end
@@ -3,14 +3,7 @@
3
3
  require 'faraday'
4
4
  require 'logging'
5
5
  require 'securerandom'
6
- # require 'vox/http/routes/audit_log'
7
- # require 'vox/http/routes/channel'
8
- # require 'vox/http/routes/emoji'
9
- # require 'vox/http/routes/guild'
10
- # require 'vox/http/routes/invite'
11
- # require 'vox/http/routes/user'
12
- # require 'vox/http/routes/voice'
13
- # require 'vox/http/routes/webhook'
6
+ require 'vox'
14
7
  require 'vox/http/route'
15
8
  require 'vox/http/routes'
16
9
  require 'vox/http/error'
@@ -4,6 +4,7 @@ require 'vox/http/routes/audit_log'
4
4
  require 'vox/http/routes/channel'
5
5
  require 'vox/http/routes/emoji'
6
6
  require 'vox/http/routes/guild'
7
+ require 'vox/http/routes/gateway'
7
8
  require 'vox/http/routes/invite'
8
9
  require 'vox/http/routes/user'
9
10
  require 'vox/http/routes/voice'
@@ -15,7 +16,7 @@ module Vox
15
16
  module Routes
16
17
  # Include all route containers if this module is included
17
18
  def self.included(klass)
18
- [AuditLog, Channel, Emoji, Guild, Invite, User, Voice, Webhook].each { |m| klass.include m }
19
+ constants.each { |m| klass.include const_get(m) }
19
20
  end
20
21
  end
21
22
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vox/http/route'
4
+
5
+ module Vox
6
+ module HTTP
7
+ module Routes
8
+ # Mixin for gateway routes, used for retriving information
9
+ # about connecting to the gateway.
10
+ module Gateway
11
+ # rubocop:disable Naming/AccessorMethodName
12
+
13
+ # Fetch the URL to use for a gateway connection.
14
+ # @return [Hash<:url, String>] An object with one key `url` that maps to the URL
15
+ # for connecting to the gateway.
16
+ # @vox.api_docs https://discord.com/developers/docs/topics/gateway#get-gateway
17
+ def get_gateway
18
+ request(Route.new(:GET, '/gateway'))
19
+ end
20
+
21
+ # Fetch the URL to use for a gateway connection, with additional sharding information.
22
+ # @return [Hash{ :url => String, :shards => Integer, :session_start_limit => Hash<Symbol, Integer>}]
23
+ # An object that includes the URL to connect to the gateway with, the recommended number of shards,
24
+ # as well as a [session start limit](https://discord.com/developers/docs/topics/gateway#session-start-limit-object-session-start-limit-structure)
25
+ # object.
26
+ # @vox.api_docs https://discord.com/developers/docs/topics/gateway#get-gateway-bot
27
+ def get_gateway_bot
28
+ request(Route.new(:GET, '/gateway/bot'))
29
+ end
30
+
31
+ # rubocop:enable Naming/AccessorMethodName
32
+ end
33
+ end
34
+ end
35
+ end
@@ -8,11 +8,9 @@ module Vox
8
8
  module Util
9
9
  # Remove members from a hash that have `:undef` as values
10
10
  # @example
11
- # ```ruby
12
11
  # hash = { foo: 1, bar: :undef, baz: 2 }
13
12
  # filter_hash(hash)
14
13
  # # => { foo: 1, baz: 2 }
15
- # ```
16
14
  # @param hash [Hash] The hash to filter `:undef` members from.
17
15
  # @return [Hash] The given hash with all members with an `:undef` value removed.
18
16
  # @!visibility private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vox
4
- # Overall gem version, each component also contains its own version
5
- VERSION = '0.2.2'
4
+ # Gem version
5
+ VERSION = '0.2.3'
6
6
  end
@@ -28,15 +28,18 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ['lib']
30
30
 
31
+ spec.add_runtime_dependency 'event_emitter', '~> 0.2.6'
31
32
  spec.add_runtime_dependency 'faraday', '~> 1.0.1'
32
33
  spec.add_runtime_dependency 'logging', '~> 2.3.0'
33
34
  spec.add_runtime_dependency 'mime-types', '~> 3.3.1'
34
35
  spec.add_runtime_dependency 'multi_json', '~> 1.15.0'
36
+ spec.add_runtime_dependency 'websocket-driver', '~> 0.7.3'
35
37
  spec.add_development_dependency 'rake', '~> 12.0'
36
38
  spec.add_development_dependency 'rspec', '~> 3.0'
37
39
  spec.add_development_dependency 'rubocop', '~> 0.89.1'
38
40
  spec.add_development_dependency 'rubocop-performance', '~> 1.7.1'
39
41
  spec.add_development_dependency 'rubocop-rspec', '~> 1.42.0'
40
42
  spec.add_development_dependency 'simplecov', '~> 0.17.1'
43
+ spec.add_development_dependency 'vox-etf', '~> 0.1.6'
41
44
  spec.add_development_dependency 'yard', '~> 0.9.25'
42
45
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Carey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-29 00:00:00.000000000 Z
11
+ date: 2020-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: event_emitter
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.6
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: faraday
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: 1.15.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: websocket-driver
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.7.3
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.7.3
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: rake
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +178,20 @@ dependencies:
150
178
  - - "~>"
151
179
  - !ruby/object:Gem::Version
152
180
  version: 0.17.1
181
+ - !ruby/object:Gem::Dependency
182
+ name: vox-etf
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 0.1.6
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 0.1.6
153
195
  - !ruby/object:Gem::Dependency
154
196
  name: yard
155
197
  requirement: !ruby/object:Gem::Requirement
@@ -185,7 +227,10 @@ files:
185
227
  - LICENSE.md
186
228
  - README.md
187
229
  - Rakefile
230
+ - examples/gateway_example.rb
188
231
  - lib/vox.rb
232
+ - lib/vox/gateway/client.rb
233
+ - lib/vox/gateway/websocket.rb
189
234
  - lib/vox/http/client.rb
190
235
  - lib/vox/http/error.rb
191
236
  - lib/vox/http/middleware.rb
@@ -196,6 +241,7 @@ files:
196
241
  - lib/vox/http/routes/audit_log.rb
197
242
  - lib/vox/http/routes/channel.rb
198
243
  - lib/vox/http/routes/emoji.rb
244
+ - lib/vox/http/routes/gateway.rb
199
245
  - lib/vox/http/routes/guild.rb
200
246
  - lib/vox/http/routes/invite.rb
201
247
  - lib/vox/http/routes/user.rb
@@ -227,7 +273,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
227
273
  - !ruby/object:Gem::Version
228
274
  version: '0'
229
275
  requirements: []
230
- rubygems_version: 3.0.3
276
+ rubygems_version: 3.1.2
231
277
  signing_key:
232
278
  specification_version: 4
233
279
  summary: Discord library