disrb 0.1.3 → 0.1.4

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
@@ -5,11 +5,14 @@ require 'async'
5
5
  require 'async/http/endpoint'
6
6
  require 'async/websocket/client'
7
7
  require 'faraday'
8
+ require 'faraday/multipart'
9
+ require 'stringio'
8
10
  require_relative 'disrb/guild'
9
11
  require_relative 'disrb/logger'
10
12
  require_relative 'disrb/user'
11
13
  require_relative 'disrb/message'
12
14
  require_relative 'disrb/application_commands'
15
+ require_relative 'version'
13
16
 
14
17
  # Contains functions related to Discord snowflakes.
15
18
  class Snowflake
@@ -47,7 +50,7 @@ class Snowflake
47
50
  end
48
51
 
49
52
  # Class that contains functions that allow interacting with the Discord API.
50
- # @version 0.1.3
53
+ # @version 0.1.4
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
129
  response = DiscordApi.get(url, headers)
114
- if response.status == 200
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
@@ -238,10 +254,10 @@ class DiscordApi
238
254
  recieved_ready = false
239
255
  url = if rescue_connection.nil?
240
256
  response = DiscordApi.get("#{@base_url}/gateway")
241
- if response.status == 200
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
@@ -362,9 +378,10 @@ class DiscordApi
362
378
  data = JSON.generate(response)
363
379
  headers = { 'content-type': 'application/json' }
364
380
  response = DiscordApi.post(url, data, headers)
365
- return response if (response.status == 204 && !with_response) || (response.status == 200 && with_response)
381
+ return response if response.is_a?(Faraday::Response) &&
382
+ ((response.status == 204 && !with_response) || (response.status == 200 && with_response))
366
383
 
367
- @logger.error("Failed to respond to interaction. Response: #{response.body}")
384
+ @logger.error("Failed to respond to interaction. Response: #{response_error_body(response)}")
368
385
  response
369
386
  end
370
387
 
@@ -480,15 +497,37 @@ class DiscordApi
480
497
  intents.reduce(0) { |acc, n| acc | n }
481
498
  end
482
499
 
500
+ private
501
+
502
+ # If 'response' is a Faraday::Response object, returns response.body, else, returns 'Empty'
503
+ # @param response [Object] Any object
504
+ # @return [String] response.body if response is a Faraday::Response object, else 'Empty'
505
+ def response_error_body(response)
506
+ return response.body if response.is_a?(Faraday::Response)
507
+
508
+ 'Empty'
509
+ end
510
+
483
511
  # Performs an HTTP GET request using Faraday.
484
512
  # @param url [String] Full URL including scheme and host; path may be included.
485
513
  # @param headers [Hash, nil] Optional request headers.
486
- # @return [Faraday::Response] The Faraday response object.
487
- def self.get(url, headers = nil)
514
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
515
+ def get(url, headers = nil)
488
516
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
489
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform GET request.") if split_url.empty?
517
+ if split_url.empty?
518
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform GET request.")
519
+ return
520
+ end
490
521
  host = split_url[0]
491
522
  path = split_url[1] if split_url[1]
523
+ if headers.is_a?(Hash)
524
+ headers['User-Agent'] = @user_agent
525
+ elsif headers.nil?
526
+ headers = { 'User-Agent' => @user_agent }
527
+ else
528
+ @logger.warn('Invalid headers parameter. It will be discarded.')
529
+ headers = { 'User-Agent' => @user_agent }
530
+ end
492
531
  conn = Faraday.new(url: host, headers: headers)
493
532
  if path
494
533
  conn.get(path)
@@ -500,12 +539,23 @@ class DiscordApi
500
539
  # Performs an HTTP DELETE request using Faraday.
501
540
  # @param url [String] Full URL including scheme and host; path may be included.
502
541
  # @param headers [Hash, nil] Optional request headers.
503
- # @return [Faraday::Response] The Faraday response object.
504
- def self.delete(url, headers = nil)
542
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
543
+ def delete(url, headers = nil)
505
544
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
506
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform DELETE request.") if split_url.empty?
545
+ if split_url.empty?
546
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform DELETE request.")
547
+ return
548
+ end
507
549
  host = split_url[0]
508
550
  path = split_url[1] if split_url[1]
551
+ if headers.is_a?(Hash)
552
+ headers['User-Agent'] = @user_agent
553
+ elsif headers.nil?
554
+ headers = { 'User-Agent' => @user_agent }
555
+ else
556
+ @logger.warn('Invalid headers parameter. It will be discarded.')
557
+ headers = { 'User-Agent' => @user_agent }
558
+ end
509
559
  conn = Faraday.new(url: host, headers: headers)
510
560
  if path
511
561
  conn.delete(path)
@@ -518,12 +568,23 @@ class DiscordApi
518
568
  # @param url [String] Full URL including scheme and host; path may be included.
519
569
  # @param data [String] Serialized request body (e.g., JSON string).
520
570
  # @param headers [Hash, nil] Optional request headers.
521
- # @return [Faraday::Response] The Faraday response object.
522
- def self.post(url, data, headers = nil)
571
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
572
+ def post(url, data, headers = nil)
523
573
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
524
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform POST request.") if split_url.empty?
574
+ if split_url.empty?
575
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform POST request.")
576
+ return
577
+ end
525
578
  host = split_url[0]
526
579
  path = split_url[1] if split_url[1]
580
+ if headers.is_a?(Hash)
581
+ headers['User-Agent'] = @user_agent
582
+ elsif headers.nil?
583
+ headers = { 'User-Agent' => @user_agent }
584
+ else
585
+ @logger.warn('Invalid headers parameter. It will be discarded.')
586
+ headers = { 'User-Agent' => @user_agent }
587
+ end
527
588
  conn = Faraday.new(url: host, headers: headers)
528
589
  if path
529
590
  conn.post(path, data)
@@ -536,12 +597,23 @@ class DiscordApi
536
597
  # @param url [String] Full URL including scheme and host; path may be included.
537
598
  # @param data [String] Serialized request body (e.g., JSON string).
538
599
  # @param headers [Hash, nil] Optional request headers.
539
- # @return [Faraday::Response] The Faraday response object.
540
- def self.patch(url, data, headers = nil)
600
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
601
+ def patch(url, data, headers = nil)
541
602
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
542
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PATCH request.") if split_url.empty?
603
+ if split_url.empty?
604
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PATCH request.")
605
+ return
606
+ end
543
607
  host = split_url[0]
544
608
  path = split_url[1] if split_url[1]
609
+ if headers.is_a?(Hash)
610
+ headers['User-Agent'] = @user_agent
611
+ elsif headers.nil?
612
+ headers = { 'User-Agent' => @user_agent }
613
+ else
614
+ @logger.warn('Invalid headers parameter. It will be discarded.')
615
+ headers = { 'User-Agent' => @user_agent }
616
+ end
545
617
  conn = Faraday.new(url: host, headers: headers)
546
618
  if path
547
619
  conn.patch(path, data)
@@ -554,12 +626,23 @@ class DiscordApi
554
626
  # @param url [String] Full URL including scheme and host; path may be included.
555
627
  # @param data [String] Serialized request body (e.g., JSON string).
556
628
  # @param headers [Hash, nil] Optional request headers.
557
- # @return [Faraday::Response] The Faraday response object.
558
- def self.put(url, data, headers = nil)
629
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
630
+ def put(url, data, headers = nil)
559
631
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
560
- @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PUT request.") if split_url.empty?
632
+ if split_url.empty?
633
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PUT request.")
634
+ return
635
+ end
561
636
  host = split_url[0]
562
637
  path = split_url[1] if split_url[1]
638
+ if headers.is_a?(Hash)
639
+ headers['User-Agent'] = @user_agent
640
+ elsif headers.nil?
641
+ headers = { 'User-Agent' => @user_agent }
642
+ else
643
+ @logger.warn('Invalid headers parameter. It will be discarded.')
644
+ headers = { 'User-Agent' => @user_agent }
645
+ end
563
646
  conn = Faraday.new(url: host, headers: headers)
564
647
  if path
565
648
  conn.put(path, data)
@@ -567,4 +650,80 @@ class DiscordApi
567
650
  conn.put('', data)
568
651
  end
569
652
  end
653
+
654
+ # Sends a HTTP POST request to the specified URL, containing multipart/form-data data structured
655
+ # according to Discord documentation.
656
+ # See https://docs.discord.com/developers/reference#uploading-files
657
+ # @param url [String] Full URL including scheme and host; path may be included.
658
+ # @param payload_json [String] JSON data which will be included in the request under the 'payload_json'
659
+ # Content-Disposition.
660
+ # @param files [Array] An array of arrays, each inner-array first has its filename (index 0),
661
+ # raw file data as a string (index 1), and then the MIME type of the file (index 2).
662
+ # @param headers [Hash, nil] Optional request headers.
663
+ # @return [Faraday::Response, nil] The Faraday response object, or nil if an error was encountered.
664
+ def file_upload(url, files, payload_json: nil, headers: nil)
665
+ split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
666
+ if split_url.empty?
667
+ @logger.error("Empty/invalid URL provided: #{url}. Cannot perform Discord multipart/form-data POST request.")
668
+ return
669
+ end
670
+ host = split_url[0]
671
+ path = split_url[1] if split_url[1]
672
+ if headers.is_a?(Hash)
673
+ headers['User-Agent'] = @user_agent
674
+ elsif headers.nil?
675
+ headers = { 'User-Agent' => @user_agent }
676
+ else
677
+ @logger.warn('Invalid headers parameter. It will be discarded.')
678
+ headers = { 'User-Agent' => @user_agent }
679
+ end
680
+ conn = Faraday.new(url: host, headers: headers, request: :multipart, content_type: 'multipart/form-data')
681
+ payload = {}
682
+ # FilePart expects File/IO objects as the first argument.
683
+ # However, since the function is being given raw data instead of File/IO objects, we should be using ParamPart
684
+ # But, ParamPart doesn't let us use a customized Content-Disposition, which is what we need
685
+ # So we will just have to wrap the raw data in an IO class with StringIO
686
+ if payload_json
687
+ payload[:payload_json] = Faraday::Multipart::FilePart.new(
688
+ StringIO.new(payload_json),
689
+ 'application/json',
690
+ nil,
691
+ 'Content-Disposition' => 'form-data; name="payload_json"'
692
+ )
693
+ end
694
+ files.each_with_index do |(filename, raw_bytes, mime_type), i|
695
+ payload[:"file_#{i}"] = Faraday::Multipart::FilePart.new(
696
+ StringIO.new(raw_bytes),
697
+ mime_type,
698
+ filename,
699
+ 'Content-Disposition' => "form-data; name=\"files[#{i}]\"; filename=\"#{filename}\""
700
+ )
701
+ end
702
+ if payload.empty?
703
+ @logger.warn("Payload empty, not sending Discord multipart/form-data POST request to #{url}.")
704
+ nil
705
+ elsif path
706
+ conn.post(path, payload)
707
+ else
708
+ conn.post('', payload)
709
+ end
710
+ end
711
+
712
+ # Generates an array of attachments objects (hashes) according to
713
+ # https://docs.discord.com/developers/resources/message#attachment-object.
714
+ # @param attachments_array [Array] An array of arrays, each inner-array first has its filename (index 0),
715
+ # raw file data as a string (index 1), and then the MIME type of the file (index 2).
716
+ # @return [Array] An array formed of Discord attachment objects (hashes)
717
+ def generate_attachment_object_array(attachments_array)
718
+ final_array = []
719
+ attachments_array.each_with_index do |(filename, raw_bytes, mime_type), i|
720
+ final_array << {
721
+ 'id' => i,
722
+ 'filename' => filename,
723
+ 'content_type' => mime_type,
724
+ 'size' => raw_bytes.bytesize
725
+ }
726
+ end
727
+ final_array
728
+ end
570
729
  end
data/lib/version.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DiscordApi
4
+ VERSION = '0.1.3'
5
+ 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
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