discorb 0.4.2 → 0.5.0

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: 7322e6131e0e5d070df790e4a4b7ea6ea6ea7a286c975b0732c0c7eff2e9903d
4
- data.tar.gz: 045ef3ab63628e0e0fe867de4aef8b1f872076cd74912b38e035c8cb6907c731
3
+ metadata.gz: 26ef2cd9945ad11a9816b4ece328391c265b8f59f8b9a67e66326e21414f083c
4
+ data.tar.gz: 06662ff1518386e95e615afffc6b8338cb9e337bb0dc36c66f6d1d79fd7a01e8
5
5
  SHA512:
6
- metadata.gz: f2b045e2b31b6a8fb95d74f87de931b30e67c8f34f647a0994b4feb4f3c3d8c270f2fddab03e995cce4eaf2f770d2698ae537e0e5d328152dc0244f3e6f5c59d
7
- data.tar.gz: 3582014d604c85091c3c6180a13766ccab7dce8e0df0319278eb0d89e7cd0d0df2d236b3a58763cc55e5ccf80ef6c46317ad62028c30f0d89002bf48f1bc4b8b
6
+ metadata.gz: 83ddc5818285d4ed50e08998446a5fa9763cffe9c0a1992a70085af38a323e304851ca4dd77f43a8108e4ece850f097eaca9dc119b83c9bdb9a66e5c32b40f5a
7
+ data.tar.gz: 8a409f031d896344586bb2001019b9431299683a99df36f090f3a0da748d08bd6b4f2191c0a32823106d5b3b0cd6d22624106367d8e4adc400acf430ddb60370
data/.yardopts CHANGED
@@ -3,6 +3,7 @@
3
3
  --markup markdown
4
4
  --tag flags:"Flags"
5
5
  --verbose
6
+ --asset docs/assets:assets
6
7
  -
7
8
  docs/**/*.md
8
9
  Changelog.md
data/Changelog.md CHANGED
@@ -98,4 +98,11 @@
98
98
 
99
99
  ## 0.4.2
100
100
 
101
- - Fix: Fix error in `discorb run`
101
+ - Fix: Fix error in `discorb run`
102
+
103
+ ## 0.5.0
104
+
105
+ - Change: Use zlib stream instead
106
+ - Add: Add tutorials
107
+ - Add: Add ratelimit handler
108
+ - Change: Make `--git` option in `discorb init` false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- discorb (0.4.2)
4
+ discorb (0.5.0)
5
5
  async (~> 1.30.1)
6
6
  async-http (~> 0.56.5)
7
7
  async-websocket (~> 0.19.0)
data/Rakefile CHANGED
@@ -117,6 +117,7 @@ namespace :document do
117
117
  iputs "Replacing CRLF with LF"
118
118
  Dir.glob("doc/**/*.*") do |file|
119
119
  next unless File.file?(file)
120
+ next unless %w[html css js].include? file.split(".").last
120
121
 
121
122
  content = ""
122
123
  File.open(file, "rb") do |f|
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/docs/cli/init.md CHANGED
@@ -30,7 +30,7 @@ Default to true.
30
30
  Whether to initialize git.
31
31
  If true, the command will initialize git and commit the initial files with commit message `Initial commit`.
32
32
  Use `git commit --amend -m "..."` to change the commit message.
33
- Default to true.
33
+ Default to false.
34
34
 
35
35
  ### `-t`, `--token`
36
36
 
data/docs/cli.md CHANGED
@@ -21,5 +21,6 @@ Currently, discorb has the following commands:
21
21
  | {file:docs/cli/run.md `run`} | Run a client. |
22
22
  | {file:docs/cli/setup.md `setup`} | Setup application commands. |
23
23
  | `show` | Show your environment. (No document) |
24
+ | `about` | Show discorb's information. (No document) |
24
25
 
25
26
  Click the command name to see the document.
data/docs/tutorial.md ADDED
@@ -0,0 +1,194 @@
1
+ # @title Tutorial
2
+
3
+ # Tutorial
4
+
5
+ Welcome to discorb! This lib allows you to create a discord bot with ease. So, let's get started!
6
+
7
+ ## Requirements
8
+
9
+ - Ruby 3.0.0+
10
+ - Basic knowledge of ruby
11
+ These documents will help you:
12
+ - [Ruby in Twenty Minutes](https://www.ruby-lang.org/en/documentation/quickstart/)
13
+ - [Ruby from other languages](https://www.ruby-lang.org/en/documentation/ruby-from-other-languages/)
14
+ - [Try ruby!](https://try.ruby-lang.org/)
15
+
16
+ ### Recommended
17
+
18
+ - Good editor
19
+ They are recommended:
20
+ - [VSCode](https://code.visualstudio.com/)
21
+ - [Atom](https://atom.io/)
22
+ - [Sublime Text](https://www.sublimetext.com/)
23
+ - [Brackets](https://brackets.io/)
24
+ - [Notepad++](https://notepad-plus-plus.org/)
25
+ - Git
26
+ - Bundler
27
+
28
+ Once you have all of these, you can start coding!
29
+
30
+ ## Start creating your bot
31
+
32
+ ### Create a Bot account
33
+
34
+ You must have a bot account to use this lib. First, go to [Discord Developer Portal](https://discord.com/developers/applications) and click on `New Application`.
35
+ ![](./assets/01_new_app.png)
36
+
37
+ And then type a name for your bot, and click `Create`.
38
+ ![](./assets/02_bot_name.png)
39
+
40
+ You will be redirected to the `General Information` page.
41
+ Then, click `Bot` and then `Add Bot` and then `Yes, do it!`.
42
+ ![](./assets/03_add_bot.png)
43
+
44
+ You will see bot information, and click `Copy` button in TOKEN section.
45
+ ![](./assets/04_token_copy.png)
46
+
47
+ **DO NOT SHARE THIS TOKEN, OR YOUR BOT BAN EVERYONE IN EVERY SERVER!**
48
+ This is serious security risk.
49
+
50
+ Click `Regenerate` button to regenerate your token. Do this immediately when you accidentally share your token.
51
+
52
+ You did it! Now, you have a bot account.
53
+
54
+ #### Invite your bot to a server
55
+
56
+ Go to `OAuth2` page and scroll down, and check `bot` and `applications.commands` permissions.
57
+ ![](./assets/05_oauth.png)
58
+ Then, click `Copy` button and paste it to your browser.
59
+ Choose a server you want to invite your bot to, and follow the instructions.
60
+
61
+ ### Code your bot
62
+
63
+ #### Install gems
64
+
65
+ Open terminal and type:
66
+
67
+ ```
68
+ gem install bundler discorb
69
+ ```
70
+
71
+ #### Setup files
72
+
73
+ Create a new directory and go to it.
74
+ Open terminal and type:
75
+
76
+ ```
77
+ discorb init
78
+ ```
79
+
80
+ Specify `--git` if you want to use git.
81
+
82
+ You will get some files in your directory.
83
+
84
+ - `main.rb`: The main file of your bot.
85
+ - `.env`: The environment variables of your bot. **You must keep this file secret!**
86
+ - `Gemfile`: Gemfile for bundler.
87
+ - `Gemfile.lock`: Gemfile.lock for bundler.
88
+
89
+ You will get other files if you specify `--git`.
90
+ {file:docs/cli/init.md Learn more here}.
91
+
92
+ #### Start your bot
93
+
94
+ Open `main.rb`, you will see the following code:
95
+
96
+ ```ruby
97
+ require "discorb"
98
+ require "dotenv"
99
+
100
+ Dotenv.load # Loads .env file
101
+
102
+ client = Discorb::Client.new # Create client for connecting to Discord
103
+
104
+ client.once :ready do
105
+ puts "Logged in as #{client.user}" # Prints username of logged in user
106
+ end
107
+
108
+ client.run ENV["TOKEN"] # Starts client
109
+ ```
110
+
111
+ Open `.env`, you will see:
112
+
113
+ ```
114
+ TOKEN=Y0urB0tT0k3nHer3.Th1sT0ken.W0ntWorkB3c4useItH4sM34n1ng
115
+ ```
116
+
117
+ Replace `Y0urB0tT0k3nHer3.Th1sT0ken.W0ntWorkB3c4useItH4sM34n1ng` with your bot token.
118
+ Remember to keep this file secret!
119
+
120
+ Open terminal and type:
121
+
122
+ ```sh
123
+ bundle exec ruby main.rb
124
+ # or
125
+ bundle exec discorb run main.rb
126
+ ```
127
+
128
+ Yay! Your bot is online!
129
+ ![](./assets/06_online.png)
130
+
131
+ But your bot won't do anything.
132
+ So add your bot some greetings!
133
+ `Ctrl + C` to stop your bot.
134
+
135
+ #### Add a greeting
136
+
137
+ You can do some action on message by typing like this:
138
+
139
+ ```ruby
140
+ client.on :message do |message|
141
+ # ...
142
+ end
143
+ ```
144
+
145
+ `message` is a {Discorb::Message} object. It contains information about the message.
146
+ You can get the message content by {Discorb::Message#content}.
147
+ Add `if` statement, and reply to the message with {Discorb::Message#reply}.
148
+
149
+ ```ruby
150
+ client.on :message do |message|
151
+ if message.content.downcase.include? "hello"
152
+ message.reply "Hello!"
153
+ end
154
+ end
155
+ ```
156
+
157
+ Save your bot and restart it.
158
+
159
+ You can see your bot's response by typing `hello` in your server...
160
+
161
+ ![](./assets/07_hello_infinite.png)
162
+
163
+ Oh no! Your bot is responding to bot's messages, and it doesn't stop!
164
+
165
+ Terminate your bot by typing `Ctrl + C` in terminal.
166
+
167
+ #### Ignore bot's messages
168
+
169
+ You can access author information by {Discorb::Message#author}, and it has {Discorb::User#bot?}.
170
+ So, you can ignore bot's messages by adding `if` statement:
171
+
172
+ ```ruby
173
+ client.on :message do |message|
174
+ next if message.author.bot?
175
+
176
+ # ...
177
+ end
178
+ ```
179
+
180
+ Note you must use `next` to exit the block.
181
+
182
+ Save your bot and start it.
183
+
184
+ ![](./assets/08_hello_once.png)
185
+
186
+ You did it! Your bot won't respond to bot's messages anymore.
187
+
188
+ ## Finally
189
+
190
+ This is the end of tutorial.
191
+
192
+ To learn more, check out the [documentation](https://discorb-lib.github.io/).
193
+
194
+ We hope you enjoy this lib! Thanks for reading!
@@ -4,7 +4,7 @@ module Discorb
4
4
  # @return [String] The API base URL.
5
5
  API_BASE_URL = "https://discord.com/api/v9"
6
6
  # @return [String] The version of discorb.
7
- VERSION = "0.4.2"
7
+ VERSION = "0.5.0"
8
8
  # @return [String] The user agent for the bot.
9
9
  USER_AGENT = "DiscordBot (https://github.com/discorb-lib/discorb #{VERSION}) Ruby/#{RUBY_VERSION}"
10
10
 
@@ -7,6 +7,7 @@ informations = {
7
7
  "Documentation" => "https://discorb-lib.github.io",
8
8
  "RubyGems" => "https://rubygems.org/gems/discorb",
9
9
  "Changelog" => "https://discorb-lib.github.io/file.Changelog.html",
10
+ "License" => "MIT License",
10
11
  }
11
12
 
12
13
  informations.each do |key, value|
@@ -1,6 +1,7 @@
1
1
  # description: Make files for the discorb project.
2
2
 
3
3
  require "optparse"
4
+ require "discorb"
4
5
  require_relative "../utils/colored_puts"
5
6
 
6
7
  $path = Dir.pwd
@@ -11,15 +12,15 @@ FILES = {
11
12
  require "discorb"
12
13
  require "dotenv"
13
14
 
14
- Dotenv.load
15
+ Dotenv.load # Loads .env file
15
16
 
16
- client = Discorb::Client.new
17
+ client = Discorb::Client.new # Create client for connecting to Discord
17
18
 
18
19
  client.once :ready do
19
- puts "Logged in as #{client.user}"
20
+ puts "Logged in as #{client.user}" # Prints username of logged in user
20
21
  end
21
22
 
22
- client.run ENV["%<token>s"]
23
+ client.run ENV["%<token>s"] # Starts client
23
24
  RUBY
24
25
  ".env" => <<~BASH,
25
26
  %<token>s=Y0urB0tT0k3nHer3.Th1sT0ken.W0ntWorkB3c4useItH4sM34n1ng
@@ -90,9 +91,9 @@ FILES = {
90
91
 
91
92
  source "https://rubygems.org"
92
93
 
93
- git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
94
+ git_source(:github) { |repo_name| "https://github.com/\#{repo_name}" }
94
95
 
95
- gem "discorb", "~> 0.2.5"
96
+ gem "discorb", "~> #{Discorb::VERSION}"
96
97
  gem "dotenv", "~> 2.7"
97
98
  RUBY
98
99
  }
@@ -141,7 +142,7 @@ opt = OptionParser.new <<~BANNER
141
142
 
142
143
  $values = {
143
144
  bundle: true,
144
- git: true,
145
+ git: false,
145
146
  force: false,
146
147
  token: "TOKEN",
147
148
  }
@@ -150,7 +151,7 @@ opt.on("--[no-]bundle", "Whether to use bundle. Default to true.") do |v|
150
151
  $values[:bundle] = v
151
152
  end
152
153
 
153
- opt.on("--[no-]git", "Whether to initialize git. Default to true.") do |v|
154
+ opt.on("--[no-]git", "Whether to initialize git. Default to false.") do |v|
154
155
  $values[:git] = v
155
156
  end
156
157
 
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "async/http"
4
+ require "async/websocket"
5
+ require "json"
6
+ require "zlib"
4
7
 
5
8
  module Discorb
6
9
  #
@@ -464,13 +467,20 @@ module Discorb
464
467
  @first = first
465
468
  _, gateway_response = @http.get("/gateway").wait
466
469
  gateway_url = gateway_response[:url]
467
- endpoint = Async::HTTP::Endpoint.parse("#{gateway_url}?v=9&encoding=json",
470
+ endpoint = Async::HTTP::Endpoint.parse("#{gateway_url}?v=9&encoding=json&compress=zlib-stream",
468
471
  alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
469
472
  begin
470
- Async::WebSocket::Client.connect(endpoint, headers: [["User-Agent", Discorb::USER_AGENT]]) do |connection|
473
+ Async::WebSocket::Client.connect(endpoint, headers: [["User-Agent", Discorb::USER_AGENT]], handler: RawConnection) do |connection|
471
474
  @connection = connection
475
+ @zlib_stream = Zlib::Inflate.new(Zlib::MAX_WBITS)
476
+ @buffer = +""
472
477
  while (message = @connection.read)
473
- handle_gateway(message)
478
+ @buffer << message
479
+ if message.end_with?((+"\x00\x00\xff\xff").force_encoding("ASCII-8BIT"))
480
+ message = JSON.parse(@zlib_stream.inflate(@buffer), symbolize_names: true)
481
+ @buffer = +""
482
+ handle_gateway(message)
483
+ end
474
484
  end
475
485
  end
476
486
  rescue Protocol::WebSocket::ClosedError => e
@@ -490,7 +500,7 @@ module Discorb
490
500
  end
491
501
 
492
502
  def send_gateway(opcode, **value)
493
- @connection.write({ op: opcode, d: value })
503
+ @connection.write({ op: opcode, d: value }.to_json)
494
504
  @connection.flush
495
505
  @log.debug "Sent message with opcode #{opcode}: #{value.to_json.gsub(@token, "[Token]")}"
496
506
  end
@@ -548,7 +558,7 @@ module Discorb
548
558
  task.sleep((interval / 1000.0 - 1) * rand)
549
559
  loop do
550
560
  @heartbeat_before = Time.now.to_f
551
- @connection.write({ op: 1, d: @last_s })
561
+ @connection.write({ op: 1, d: @last_s }.to_json)
552
562
  @connection.flush
553
563
  @log.debug "Sent opcode 1."
554
564
  @log.debug "Waiting for heartbeat."
@@ -973,5 +983,25 @@ module Discorb
973
983
  end
974
984
  end
975
985
  end
986
+
987
+ #
988
+ # A class for connecting websocket with raw bytes data.
989
+ # @private
990
+ #
991
+ class RawConnection < Async::WebSocket::Connection
992
+ def initialize(...)
993
+ super
994
+ end
995
+
996
+ def parse(buffer)
997
+ # noop
998
+ buffer.to_s
999
+ end
1000
+
1001
+ def dump(object)
1002
+ # noop
1003
+ object.to_s
1004
+ end
1005
+ end
976
1006
  end
977
1007
  end
data/lib/discorb/http.rb CHANGED
@@ -13,6 +13,7 @@ module Discorb
13
13
  # @!visibility private
14
14
  def initialize(client)
15
15
  @client = client
16
+ @ratelimit_handler = RatelimitHandler.new(client)
16
17
  end
17
18
 
18
19
  #
@@ -31,8 +32,10 @@ module Discorb
31
32
  #
32
33
  def get(path, headers: nil, audit_log_reason: nil, **kwargs)
33
34
  Async do |task|
35
+ @ratelimit_handler.wait("GET", path)
34
36
  resp = http.get(get_path(path), get_headers(headers, "", audit_log_reason), **kwargs)
35
37
  data = get_response_data(resp)
38
+ @ratelimit_handler.save("GET", path, resp)
36
39
  test_error(if resp.code == "429"
37
40
  @client.log.warn "Ratelimit exceeded for #{path}, trying again in #{data[:retry_after]} seconds."
38
41
  task.sleep(data[:retry_after])
@@ -60,8 +63,10 @@ module Discorb
60
63
  #
61
64
  def post(path, body = "", headers: nil, audit_log_reason: nil, **kwargs)
62
65
  Async do |task|
66
+ @ratelimit_handler.wait("POST", path)
63
67
  resp = http.post(get_path(path), get_body(body), get_headers(headers, body, audit_log_reason), **kwargs)
64
68
  data = get_response_data(resp)
69
+ @ratelimit_handler.save("POST", path, resp)
65
70
  test_error(if resp.code == "429"
66
71
  task.sleep(data[:retry_after])
67
72
  post(path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
@@ -88,13 +93,10 @@ module Discorb
88
93
  #
89
94
  def patch(path, body = "", headers: nil, audit_log_reason: nil, **kwargs)
90
95
  Async do |task|
96
+ @ratelimit_handler.wait("PATCH", path)
91
97
  resp = http.patch(get_path(path), get_body(body), get_headers(headers, body, audit_log_reason), **kwargs)
92
- rd = resp.body
93
- data = if rd.nil? || rd.empty?
94
- nil
95
- else
96
- JSON.parse(rd, symbolize_names: true)
97
- end
98
+ data = get_response_data(resp)
99
+ @ratelimit_handler.save("PATCH", path, resp)
98
100
  test_error(if resp.code == "429"
99
101
  task.sleep(data[:retry_after])
100
102
  patch(path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
@@ -121,8 +123,10 @@ module Discorb
121
123
  #
122
124
  def put(path, body = "", headers: nil, audit_log_reason: nil, **kwargs)
123
125
  Async do |task|
126
+ @ratelimit_handler.wait("PUT", path)
124
127
  resp = http.put(get_path(path), get_body(body), get_headers(headers, body, audit_log_reason), **kwargs)
125
128
  data = get_response_data(resp)
129
+ @ratelimit_handler.save("PUT", path, resp)
126
130
  test_error(if resp.code == "429"
127
131
  task.sleep(data[:retry_after])
128
132
  put(path, body, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
@@ -148,8 +152,10 @@ module Discorb
148
152
  #
149
153
  def delete(path, headers: nil, audit_log_reason: nil, **kwargs)
150
154
  Async do |task|
155
+ @ratelimit_handler.wait("DELETE", path)
151
156
  resp = http.delete(get_path(path), get_headers(headers, "", audit_log_reason))
152
157
  data = get_response_data(resp)
158
+ @ratelimit_handler.save("DELETE", path, resp)
153
159
  test_error(if resp.code == "429"
154
160
  task.sleep(data[:retry_after])
155
161
  delete(path, headers: headers, audit_log_reason: audit_log_reason, **kwargs).wait
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discorb
4
+ #
5
+ # Class to handle rate limiting.
6
+ #
7
+ class RatelimitHandler
8
+ def initialize(client)
9
+ @client = client
10
+ @ratelimit_hash = {}
11
+ @path_ratelimit_hash = {}
12
+ end
13
+
14
+ def wait(method, path)
15
+ return if path.start_with?("https://")
16
+
17
+ return unless hash = @path_ratelimit_hash[method + path]
18
+
19
+ return unless b = @ratelimit_hash[hash]
20
+
21
+ if b[:reset_at] < Time.now.to_i
22
+ @ratelimit_hash.delete(hash)
23
+ return
24
+ end
25
+ return if b[:remaining] > 0
26
+
27
+ @client.log.info("Ratelimit reached, waiting for #{b[:reset_at] - Time.now.to_i} seconds")
28
+ sleep(b[:reset_at] - Time.now.to_i)
29
+ end
30
+
31
+ def save(method, path, resp)
32
+ return unless resp["X-RateLimit-Remaining"]
33
+
34
+ @path_ratelimit_hash[method + path] = resp["X-RateLimit-Bucket"]
35
+ @ratelimit_hash[resp["X-RateLimit-Bucket"]] = {
36
+ remaining: resp["X-RateLimit-Remaining"].to_i,
37
+ reset_at: resp["X-RateLimit-Reset"].to_i,
38
+ }
39
+ end
40
+ end
41
+ end
data/lib/discorb.rb CHANGED
@@ -39,7 +39,7 @@ module Discorb
39
39
  end
40
40
  end
41
41
 
42
- require_order = %w[common flag dictionary error http intents emoji_table modules] +
42
+ require_order = %w[common flag dictionary error rate_limit http intents emoji_table modules] +
43
43
  %w[user member guild emoji channel embed message] +
44
44
  %w[application audit_logs color components event extension] +
45
45
  %w[file guild_template image integration interaction invite log permission] +
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: discorb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sevenc-nanashi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-08 00:00:00.000000000 Z
11
+ date: 2021-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -89,6 +89,15 @@ files:
89
89
  - discorb.gemspec
90
90
  - docs/Examples.md
91
91
  - docs/application_command.md
92
+ - docs/assets/01_new_app.png
93
+ - docs/assets/02_bot_name.png
94
+ - docs/assets/03_add_bot.png
95
+ - docs/assets/04_token_copy.png
96
+ - docs/assets/05_oauth.png
97
+ - docs/assets/06_online.png
98
+ - docs/assets/07_hello_infinite.png
99
+ - docs/assets/08_hello_once.png
100
+ - docs/assets/unused_ping_pong.png
92
101
  - docs/cli.md
93
102
  - docs/cli/init.md
94
103
  - docs/cli/irb.md
@@ -96,6 +105,7 @@ files:
96
105
  - docs/cli/setup.md
97
106
  - docs/events.md
98
107
  - docs/extension.md
108
+ - docs/tutorial.md
99
109
  - docs/voice_events.md
100
110
  - examples/commands/bookmarker.rb
101
111
  - examples/commands/hello.rb
@@ -151,6 +161,7 @@ files:
151
161
  - lib/discorb/modules.rb
152
162
  - lib/discorb/permission.rb
153
163
  - lib/discorb/presence.rb
164
+ - lib/discorb/rate_limit.rb
154
165
  - lib/discorb/reaction.rb
155
166
  - lib/discorb/role.rb
156
167
  - lib/discorb/sticker.rb