vox 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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