discorb 0.4.2 → 0.5.0

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: 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