disrb 0.1.3 → 0.1.4.1

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.
data/lib/disrb.rb CHANGED
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'uri'
4
5
  require 'async'
5
6
  require 'async/http/endpoint'
6
7
  require 'async/websocket/client'
7
8
  require 'faraday'
9
+ require 'faraday/multipart'
10
+ require 'stringio'
8
11
  require_relative 'disrb/guild'
9
12
  require_relative 'disrb/logger'
10
13
  require_relative 'disrb/user'
11
14
  require_relative 'disrb/message'
12
15
  require_relative 'disrb/application_commands'
16
+ require_relative 'version'
13
17
 
14
18
  # Contains functions related to Discord snowflakes.
15
19
  class Snowflake
@@ -47,7 +51,6 @@ class Snowflake
47
51
  end
48
52
 
49
53
  # Class that contains functions that allow interacting with the Discord API.
50
- # @version 0.1.3
51
54
  class DiscordApi
52
55
  # @!attribute [r] base_url
53
56
  # @return [String] the base URL that is used to access the Discord API. ex: "https://discord.com/api/v10"
@@ -55,13 +58,16 @@ class DiscordApi
55
58
  # @return [String] the authorization header that is used to authenticate requests to the Discord API.
56
59
  # @!attribute [r] application_id
57
60
  # @return [Integer] the application ID of the bot that has been assigned to the provided authorization token.
58
- attr_accessor(:base_url, :authorization_header, :application_id, :logger)
61
+ attr_accessor(:base_url, :authorization_header, :application_id, :logger, :user_agent)
59
62
 
60
63
  # Creates a new DiscordApi instance. (required to use most functions)
61
64
  #
62
65
  # @param authorization_token_type [String] The type of authorization token provided by Discord, 'Bot' or 'Bearer'.
63
66
  # @param authorization_token [String] The value of the authorization token provided by Discord.
64
67
  # @param verbosity_level [String, Integer, nil] The verbosity level of the logger.
68
+ # @param user_agent [String, nil] When sending a request to Discord's HTTP API, a valid User-Agent header must be set.
69
+ # By setting this parameter, the value of the User-Agent header sent will be equal to the value of this parameter.
70
+ # Defaults to 'discord.rb (https://github.com/hoovad/discord.rb, [discord.rb version])'
65
71
  # Set verbosity_level to:
66
72
  # - 'all' or 5 to log all of the below plus debug messages
67
73
  # - 'info', 4 or nil to log all of the below plus info messages [DEFAULT]
@@ -70,7 +76,7 @@ class DiscordApi
70
76
  # - 'fatal_error' or 1 to log only fatal errors
71
77
  # - 'none' or 0 for no logging
72
78
  # @return [DiscordApi] DiscordApi instance.
73
- def initialize(authorization_token_type, authorization_token, verbosity_level = nil)
79
+ def initialize(authorization_token_type, authorization_token, verbosity_level = nil, user_agent = nil)
74
80
  @api_version = '10'
75
81
  @base_url = "https://discord.com/api/v#{@api_version}"
76
82
  @authorization_token_type = authorization_token_type
@@ -108,13 +114,23 @@ class DiscordApi
108
114
  @verbosity_level = 4
109
115
  end
110
116
  @logger = Logger2.new(@verbosity_level)
117
+ default_user_agent = "discord.rb (https://github.com/hoovad/discord.rb, #{DiscordApi::VERSION})"
118
+ if user_agent.is_a?(String) && !user_agent.empty?
119
+ @user_agent = user_agent
120
+ elsif user_agent.nil?
121
+ @user_agent = default_user_agent
122
+ else
123
+ @logger.warn("Invalid user_agent parameter. It must be a valid non-empty string. \
124
+ Defaulting to #{default_user_agent}.")
125
+ @user_agent = default_user_agent
126
+ end
111
127
  url = "#{@base_url}/applications/@me"
112
128
  headers = { 'Authorization': @authorization_header }
113
- response = DiscordApi.get(url, headers)
114
- if response.status == 200
129
+ response = get(url, headers)
130
+ if response.is_a?(Faraday::Response) && response.status == 200
115
131
  @application_id = JSON.parse(response.body)['id']
116
132
  else
117
- @logger.fatal_error("Failed to get application ID with response: #{response.body}")
133
+ @logger.fatal_error("Failed to get application ID with response: #{response_error_body(response)}")
118
134
  exit
119
135
  end
120
136
  end
@@ -237,11 +253,11 @@ class DiscordApi
237
253
  loop do
238
254
  recieved_ready = false
239
255
  url = if rescue_connection.nil?
240
- response = DiscordApi.get("#{@base_url}/gateway")
241
- if response.status == 200
256
+ response = get("#{@base_url}/gateway")
257
+ if response.is_a?(Faraday::Response) && response.status == 200
242
258
  "#{JSON.parse(response.body)['url']}/?v=#{@api_version}&encoding=json"
243
259
  else
244
- @logger.fatal_error("Failed to get gateway URL. Response: #{response.body}")
260
+ @logger.fatal_error("Failed to get gateway URL. Response: #{response_error_body(response)}")
245
261
  exit
246
262
  end
247
263
  else
@@ -353,18 +369,25 @@ class DiscordApi
353
369
  # @param interaction [Hash] The interaction payload received from the Gateway.
354
370
  # @param response [Hash] The interaction response payload.
355
371
  # @param with_response [TrueClass, FalseClass] Whether to request the created message in the response.
372
+ # @param files [Array] An array of arrays, each inner-array first has its filename (index 0),
373
+ # raw file data as a string (index 1), and then the MIME type of the file (index 2).
356
374
  # @return [Faraday::Response] The response from the Discord API.
357
- def respond_interaction(interaction, response, with_response: false)
375
+ def respond_interaction(interaction, response, with_response: false, files: nil)
358
376
  query_string_hash = {}
359
377
  query_string_hash[:with_response] = with_response
360
378
  query_string = DiscordApi.handle_query_strings(query_string_hash)
361
379
  url = "#{@base_url}/interactions/#{interaction[:d][:id]}/#{interaction[:d][:token]}/callback#{query_string}"
362
380
  data = JSON.generate(response)
363
- headers = { 'content-type': 'application/json' }
364
- response = DiscordApi.post(url, data, headers)
365
- return response if (response.status == 204 && !with_response) || (response.status == 200 && with_response)
381
+ if files
382
+ response = file_upload(url, files, payload_json: data)
383
+ else
384
+ headers = { 'Content-Type' => 'application/json' }
385
+ response = post(url, data, headers)
386
+ end
387
+ return response if response.is_a?(Faraday::Response) &&
388
+ ((response.status == 204 && !with_response) || (response.status == 200 && with_response))
366
389
 
367
- @logger.error("Failed to respond to interaction. Response: #{response.body}")
390
+ @logger.error("Failed to respond to interaction. Response: #{response_error_body(response)}")
368
391
  response
369
392
  end
370
393
 
@@ -480,91 +503,216 @@ class DiscordApi
480
503
  intents.reduce(0) { |acc, n| acc | n }
481
504
  end
482
505
 
506
+ private
507
+
508
+ # If 'response' is a Faraday::Response object, returns response.body, else, returns 'Empty'
509
+ # @param response [Object] Any object
510
+ # @return [String] response.body if response is a Faraday::Response object, else 'Empty'
511
+ def response_error_body(response)
512
+ return response.body if response.is_a?(Faraday::Response)
513
+
514
+ 'Empty'
515
+ end
516
+
517
+ # Parses a full URL and returns connection host and request path.
518
+ # @param url [String] Full URL.
519
+ # @param method_name [String] HTTP method name used for logging context.
520
+ # @return [Hash, nil] { host:, path: } or nil if URL is invalid.
521
+ def parse_request_url(url, method_name)
522
+ begin
523
+ uri = URI.parse(url.to_s)
524
+ rescue URI::InvalidURIError
525
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform #{method_name} request.")
526
+ return nil
527
+ end
528
+ if uri.scheme.nil? || uri.host.nil?
529
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform #{method_name} request.")
530
+ return nil
531
+ end
532
+
533
+ host = "#{uri.scheme}://#{uri.host}"
534
+ host = "#{host}:#{uri.port}" if uri.port && uri.port != uri.default_port
535
+ { host: host, path: uri.request_uri }
536
+ end
537
+
483
538
  # Performs an HTTP GET request using Faraday.
484
539
  # @param url [String] Full URL including scheme and host; path may be included.
485
540
  # @param headers [Hash, nil] Optional request headers.
486
- # @return [Faraday::Response] The Faraday response object.
487
- def self.get(url, headers = nil)
488
- split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
489
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform GET request.") if split_url.empty?
490
- host = split_url[0]
491
- path = split_url[1] if split_url[1]
492
- conn = Faraday.new(url: host, headers: headers)
493
- if path
494
- conn.get(path)
541
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
542
+ def get(url, headers = nil)
543
+ parsed_url = parse_request_url(url, 'GET')
544
+ return if parsed_url.nil?
545
+
546
+ host = parsed_url[:host]
547
+ path = parsed_url[:path]
548
+ if headers.is_a?(Hash)
549
+ headers['User-Agent'] = @user_agent
550
+ elsif headers.nil?
551
+ headers = { 'User-Agent' => @user_agent }
495
552
  else
496
- conn.get
553
+ @logger.warn('Invalid headers parameter. It will be discarded.')
554
+ headers = { 'User-Agent' => @user_agent }
497
555
  end
556
+ conn = Faraday.new(url: host, headers: headers)
557
+ conn.get(path)
498
558
  end
499
559
 
500
560
  # Performs an HTTP DELETE request using Faraday.
501
561
  # @param url [String] Full URL including scheme and host; path may be included.
502
562
  # @param headers [Hash, nil] Optional request headers.
503
- # @return [Faraday::Response] The Faraday response object.
504
- def self.delete(url, headers = nil)
505
- split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
506
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform DELETE request.") if split_url.empty?
507
- host = split_url[0]
508
- path = split_url[1] if split_url[1]
509
- conn = Faraday.new(url: host, headers: headers)
510
- if path
511
- conn.delete(path)
563
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
564
+ def delete(url, headers = nil)
565
+ parsed_url = parse_request_url(url, 'DELETE')
566
+ return if parsed_url.nil?
567
+
568
+ host = parsed_url[:host]
569
+ path = parsed_url[:path]
570
+ if headers.is_a?(Hash)
571
+ headers['User-Agent'] = @user_agent
572
+ elsif headers.nil?
573
+ headers = { 'User-Agent' => @user_agent }
512
574
  else
513
- conn.delete
575
+ @logger.warn('Invalid headers parameter. It will be discarded.')
576
+ headers = { 'User-Agent' => @user_agent }
514
577
  end
578
+ conn = Faraday.new(url: host, headers: headers)
579
+ conn.delete(path)
515
580
  end
516
581
 
517
582
  # Performs an HTTP POST request using Faraday.
518
583
  # @param url [String] Full URL including scheme and host; path may be included.
519
584
  # @param data [String] Serialized request body (e.g., JSON string).
520
585
  # @param headers [Hash, nil] Optional request headers.
521
- # @return [Faraday::Response] The Faraday response object.
522
- def self.post(url, data, headers = nil)
523
- split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
524
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform POST request.") if split_url.empty?
525
- host = split_url[0]
526
- path = split_url[1] if split_url[1]
527
- conn = Faraday.new(url: host, headers: headers)
528
- if path
529
- conn.post(path, data)
586
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
587
+ def post(url, data, headers = nil)
588
+ parsed_url = parse_request_url(url, 'POST')
589
+ return if parsed_url.nil?
590
+
591
+ host = parsed_url[:host]
592
+ path = parsed_url[:path]
593
+ if headers.is_a?(Hash)
594
+ headers['User-Agent'] = @user_agent
595
+ elsif headers.nil?
596
+ headers = { 'User-Agent' => @user_agent }
530
597
  else
531
- conn.post('', data)
598
+ @logger.warn('Invalid headers parameter. It will be discarded.')
599
+ headers = { 'User-Agent' => @user_agent }
532
600
  end
601
+ conn = Faraday.new(url: host, headers: headers)
602
+ conn.post(path, data)
533
603
  end
534
604
 
535
605
  # Performs an HTTP PATCH request using Faraday.
536
606
  # @param url [String] Full URL including scheme and host; path may be included.
537
607
  # @param data [String] Serialized request body (e.g., JSON string).
538
608
  # @param headers [Hash, nil] Optional request headers.
539
- # @return [Faraday::Response] The Faraday response object.
540
- def self.patch(url, data, headers = nil)
541
- split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
542
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PATCH request.") if split_url.empty?
543
- host = split_url[0]
544
- path = split_url[1] if split_url[1]
545
- conn = Faraday.new(url: host, headers: headers)
546
- if path
547
- conn.patch(path, data)
609
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
610
+ def patch(url, data, headers = nil)
611
+ parsed_url = parse_request_url(url, 'PATCH')
612
+ return if parsed_url.nil?
613
+
614
+ host = parsed_url[:host]
615
+ path = parsed_url[:path]
616
+ if headers.is_a?(Hash)
617
+ headers['User-Agent'] = @user_agent
618
+ elsif headers.nil?
619
+ headers = { 'User-Agent' => @user_agent }
548
620
  else
549
- conn.patch('', data)
621
+ @logger.warn('Invalid headers parameter. It will be discarded.')
622
+ headers = { 'User-Agent' => @user_agent }
550
623
  end
624
+ conn = Faraday.new(url: host, headers: headers)
625
+ conn.patch(path, data)
551
626
  end
552
627
 
553
628
  # Performs an HTTP PUT request using Faraday.
554
629
  # @param url [String] Full URL including scheme and host; path may be included.
555
630
  # @param data [String] Serialized request body (e.g., JSON string).
556
631
  # @param headers [Hash, nil] Optional request headers.
557
- # @return [Faraday::Response] The Faraday response object.
558
- def self.put(url, data, headers = nil)
559
- split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
560
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PUT request.") if split_url.empty?
561
- host = split_url[0]
562
- path = split_url[1] if split_url[1]
632
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
633
+ def put(url, data, headers = nil)
634
+ parsed_url = parse_request_url(url, 'PUT')
635
+ return if parsed_url.nil?
636
+
637
+ host = parsed_url[:host]
638
+ path = parsed_url[:path]
639
+ if headers.is_a?(Hash)
640
+ headers['User-Agent'] = @user_agent
641
+ elsif headers.nil?
642
+ headers = { 'User-Agent' => @user_agent }
643
+ else
644
+ @logger.warn('Invalid headers parameter. It will be discarded.')
645
+ headers = { 'User-Agent' => @user_agent }
646
+ end
563
647
  conn = Faraday.new(url: host, headers: headers)
564
- if path
565
- conn.put(path, data)
648
+ conn.put(path, data)
649
+ end
650
+
651
+ # Sends a HTTP POST/PATCH request to the specified URL, containing multipart/form-data data structured
652
+ # according to Discord documentation.
653
+ # See https://docs.discord.com/developers/reference#uploading-files
654
+ # @param url [String] Full URL including scheme and host; path may be included.
655
+ # @param payload_json [String] JSON data which will be included in the request under the 'payload_json'
656
+ # Content-Disposition.
657
+ # @param files [Array] An array of arrays, each inner-array first has its filename (index 0),
658
+ # raw file data as a string (index 1), and then the MIME type of the file (index 2).
659
+ # @param headers [Hash, nil] Optional request headers.
660
+ # @param request_type [Symbol, nil] Specify whether to make a POST (:post) or PATCH (:patch) request. POST by default.
661
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
662
+ def file_upload(url, files, payload_json: nil, headers: nil, request_type: :post)
663
+ parsed_url = parse_request_url(url, 'multipart/form-data')
664
+ return if parsed_url.nil?
665
+
666
+ host = parsed_url[:host]
667
+ path = parsed_url[:path]
668
+ if headers.is_a?(Hash)
669
+ headers['User-Agent'] = @user_agent
670
+ elsif headers.nil?
671
+ headers = { 'User-Agent' => @user_agent }
672
+ else
673
+ @logger.warn('Invalid headers parameter. It will be discarded.')
674
+ headers = { 'User-Agent' => @user_agent }
675
+ end
676
+ conn = Faraday.new(url: host, headers: headers) do |faraday|
677
+ faraday.request :multipart
678
+ end
679
+ payload = {}
680
+ payload['payload_json'] = Faraday::Multipart::ParamPart.new(payload_json, 'application/json') if payload_json
681
+ files.each_with_index do |(filename, raw_bytes, mime_type), i|
682
+ payload["files[#{i}]"] = Faraday::Multipart::FilePart.new(
683
+ StringIO.new(raw_bytes),
684
+ mime_type,
685
+ filename
686
+ )
687
+ end
688
+ if payload.empty?
689
+ @logger.warn("Payload empty, not sending Discord multipart/form-data POST request to #{url}.")
690
+ nil
566
691
  else
567
- conn.put('', data)
692
+ case request_type
693
+ when :post
694
+ conn.post(path, payload)
695
+ when :patch
696
+ conn.patch(path, payload)
697
+ end
698
+ end
699
+ end
700
+
701
+ # Generates an array of attachments objects (hashes) according to
702
+ # https://docs.discord.com/developers/resources/message#attachment-object.
703
+ # @param attachments_array [Array] An array of arrays, each inner-array first has its filename (index 0),
704
+ # raw file data as a string (index 1), and then the MIME type of the file (index 2).
705
+ # @return [Array] An array formed of Discord attachment objects (hashes)
706
+ def generate_attachment_object_array(attachments_array)
707
+ final_array = []
708
+ attachments_array.each_with_index do |(filename, raw_bytes, mime_type), i|
709
+ final_array << {
710
+ 'id' => i,
711
+ 'filename' => filename,
712
+ 'content_type' => mime_type,
713
+ 'size' => raw_bytes.bytesize
714
+ }
568
715
  end
716
+ final_array
569
717
  end
570
718
  end
data/lib/version.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @version 0.1.4.1
4
+ class DiscordApi
5
+ VERSION = '0.1.4.1'
6
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: disrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - hoovad
@@ -65,6 +65,34 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: 2.13.3
68
+ - !ruby/object:Gem::Dependency
69
+ name: faraday-multipart
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.2.0
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.2.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: stringio
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 3.2.0
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 3.2.0
68
96
  - !ruby/object:Gem::Dependency
69
97
  name: rubocop
70
98
  requirement: !ruby/object:Gem::Requirement
@@ -110,6 +138,7 @@ files:
110
138
  - lib/disrb/logger.rb
111
139
  - lib/disrb/message.rb
112
140
  - lib/disrb/user.rb
141
+ - lib/version.rb
113
142
  homepage: https://github.com/hoovad/discord.rb
114
143
  licenses:
115
144
  - MIT