discordrb 1.8.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of discordrb might be problematic. Click here for more details.

Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.overcommit.yml +7 -0
  3. data/.rubocop.yml +5 -4
  4. data/CHANGELOG.md +77 -0
  5. data/README.md +25 -15
  6. data/discordrb.gemspec +2 -3
  7. data/examples/commands.rb +14 -2
  8. data/examples/ping.rb +1 -1
  9. data/examples/pm_send.rb +1 -1
  10. data/lib/discordrb.rb +9 -0
  11. data/lib/discordrb/api.rb +176 -50
  12. data/lib/discordrb/await.rb +3 -0
  13. data/lib/discordrb/bot.rb +607 -372
  14. data/lib/discordrb/cache.rb +208 -0
  15. data/lib/discordrb/commands/command_bot.rb +50 -18
  16. data/lib/discordrb/commands/container.rb +11 -2
  17. data/lib/discordrb/commands/events.rb +2 -0
  18. data/lib/discordrb/commands/parser.rb +10 -8
  19. data/lib/discordrb/commands/rate_limiter.rb +2 -0
  20. data/lib/discordrb/container.rb +24 -25
  21. data/lib/discordrb/data.rb +521 -219
  22. data/lib/discordrb/errors.rb +6 -7
  23. data/lib/discordrb/events/await.rb +2 -0
  24. data/lib/discordrb/events/bans.rb +3 -1
  25. data/lib/discordrb/events/channels.rb +124 -0
  26. data/lib/discordrb/events/generic.rb +2 -0
  27. data/lib/discordrb/events/guilds.rb +16 -13
  28. data/lib/discordrb/events/lifetime.rb +12 -2
  29. data/lib/discordrb/events/members.rb +26 -15
  30. data/lib/discordrb/events/message.rb +20 -7
  31. data/lib/discordrb/events/presence.rb +18 -2
  32. data/lib/discordrb/events/roles.rb +83 -0
  33. data/lib/discordrb/events/typing.rb +15 -2
  34. data/lib/discordrb/events/voice_state_update.rb +2 -0
  35. data/lib/discordrb/light.rb +8 -0
  36. data/lib/discordrb/light/data.rb +62 -0
  37. data/lib/discordrb/light/integrations.rb +73 -0
  38. data/lib/discordrb/light/light_bot.rb +56 -0
  39. data/lib/discordrb/logger.rb +4 -0
  40. data/lib/discordrb/permissions.rb +16 -12
  41. data/lib/discordrb/token_cache.rb +3 -0
  42. data/lib/discordrb/version.rb +3 -1
  43. data/lib/discordrb/voice/encoder.rb +2 -0
  44. data/lib/discordrb/voice/network.rb +21 -14
  45. data/lib/discordrb/voice/voice_bot.rb +26 -3
  46. data/lib/discordrb/websocket.rb +69 -0
  47. metadata +15 -26
  48. data/lib/discordrb/events/channel_create.rb +0 -44
  49. data/lib/discordrb/events/channel_delete.rb +0 -44
  50. data/lib/discordrb/events/channel_update.rb +0 -46
  51. data/lib/discordrb/events/guild_role_create.rb +0 -35
  52. data/lib/discordrb/events/guild_role_delete.rb +0 -36
  53. data/lib/discordrb/events/guild_role_update.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a817886abdced3f0f96221e3b286feadc093e8f
4
- data.tar.gz: 8d4cbf89664f16b80d2aa800f13c8ffc565938db
3
+ metadata.gz: ca5ae97de9345328dac0b9f9e8c14f38e2d70bc9
4
+ data.tar.gz: fe60d3642cecec76594a3356d93c923be5f27997
5
5
  SHA512:
6
- metadata.gz: c2a51f5fc08c7803625df60cca4ce53e84a95041086085761ded3175d4b4287cda6b90381ac49b9605fa2a539e7b3a23bd097f01feefe0f1c71f227215900349
7
- data.tar.gz: 192d176ba53f01de7b1c24ad68e36c9317caf25665a432c24096344ddc51e15c48e52ce395bd054bc19e8a7a926750e52dcce863c4f1d89fe05ca3eb27c1220a
6
+ metadata.gz: fa6c978e90c0f5338a9f6b67f0cf1fafd6d2c847c76b1fb6aadacfb820c95248ecdb8962ab84d63c2811b0bd28dd6913b72ae1463b671822f8716d526d04e9dc
7
+ data.tar.gz: b0eab7396be2942e2c7f9370c49fa1b45e70c2836d1281bf31e9fc98bc44919ac276c6a453000758b90a502cbde65ced78782a5b5b82c5472a20f429dc710bb7
@@ -0,0 +1,7 @@
1
+ PreCommit:
2
+ RuboCop:
3
+ enabled: true
4
+ on_warn: fail # Treat all warnings as failures
5
+
6
+ AuthorName:
7
+ enabled: false
@@ -41,10 +41,11 @@ Lint/RescueException:
41
41
  AllCops:
42
42
  TargetRubyVersion: 2.1
43
43
 
44
- # Apparently setting the target version to 2.1 is not enough to prevent this cop...
45
- Style/FrozenStringLiteralComment:
46
- Enabled: false
47
-
48
44
  # http://stackoverflow.com/questions/4763121/should-i-use-alias-or-alias-method
49
45
  Style/Alias:
50
46
  Enabled: false
47
+
48
+ # So RuboCop doesn't complain about application IDs
49
+ Style/NumericLiterals:
50
+ Exclude:
51
+ - examples/*
@@ -1,5 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ This is the first major update with some breaking changes! Those are highlighted in bold with migration advice after them. Ask in the Discord channel (see the README) if you have questions.
6
+
7
+ - **Bot initializers now only use named parameters.** This shouldn't be a hard change to adjust to, but everyone will have to do it. Here's some examples:
8
+ ```rb
9
+ # Previously
10
+ bot = Discordrb::Bot.new 'email@example.com', 'hunter2', true
11
+
12
+ # Now
13
+ bot = Discordrb::Bot.new email: 'email@example.com', password: 'hunter2', log_mode: :debug
14
+ ```
15
+ ```rb
16
+ # Previously
17
+ bot = Discordrb::Bot.new :token, 'TOKEN HERE'
18
+
19
+ # Now
20
+ bot = Discordrb::Bot.new token: 'TOKEN HERE', application_id: 163456789123456789
21
+ ```
22
+ ```rb
23
+ # Previously
24
+ bot = Discordrb::Commands::CommandBot.new :token, 'TOKEN HERE', '!', nil, {advanced_functionality: false}
25
+
26
+ # Now
27
+ bot = Discordrb::Commands::CommandBot.new token: 'TOKEN HERE', application_id: 163456789123456789, prefix: '!', advanced_functionality: false
28
+ ```
29
+ - Connecting to multiple voice channels at once (only available with bot accounts) is now supported. **This means `bot.voice` now takes the server ID as the parameter**. For a seamless switch, the utility method `MessageEvent#voice` was added - simply replace `bot.voice` with `event.voice` in all instances.
30
+ - **The `Member` and `Recipient` classes were split off from `User`**. Members are users on servers and recipients are partners in private messages. Since both are delegates to `User`, most things will work as before, but most notably roles were changed to no longer be by ID (for example, instead of `event.author.roles(event.server.id)`, you'd just use `event.author.roles` instead).
31
+ - **All previously deprecated methods were removed.** This includes:
32
+ - `Server#afk_channel_id=` (use `afk_channel=`, it works with the ID too)
33
+ - `Channel#is_private` (use `private?` instead, it's more reliable with edge cases like Twitch subscriber-only channels)
34
+ - `Bot#find` (use `find_channel` instead, it does the exact same thing without confusion with `find_user`)
35
+ - **`Server` is now used instead of `Guild` in all external methods and classes.** Previously, all the events regarding roles and such were called `GuildRoleXYZEvent`, now they're all called `ServerRoleXYZEvent` for consistency with other attributes and methods.
36
+ - **`advanced_functionality` is now disabled by default.** If you absolutely need it, you can easily re-enable it by just setting that parameter in the CommandBot initializer, but for most people that didn't need it this will fix some bugs with mentions in commands and such.
37
+ - **`User#bot?` was renamed to `User#current_bot?`** with the addition of the `User#bot_account?` reader to check for bot account-ness (the "BOT" tag visible on Discord)
38
+ - Member chunks will no longer automatically be requested on startup, but rather once they're actually needed (`event.server.members`). This is both a performance change (much faster startup for large bots especially) and an important API compliance one - this is what the Discord devs have requested.
39
+ - Initial support for bots that have no WebSocket connection was started. This is useful for web apps that need to get information on something without having to run something in the background all the time. A tutorial on these will be coming soon, in the meantime, use this short example:
40
+ ```rb
41
+ require 'discordrb'
42
+ require 'discordrb/light'
43
+
44
+ bot = Discordrb::Light::LightBot.new 'token here'
45
+ puts bot.profile.username
46
+ ```
47
+ - OAuth bot accounts are now better supported using a method `Bot#invite_url` to get a bot's invite URL and sending tokens using the new `Bot` prefix.
48
+ - discordrb now fully uses [websocket-client-simple](https://github.com/shokai/websocket-client-simple) (a.k.a. WSCS) instead of Faye::WebSocket, this means that the annoying OpenSSL library thing won't have to be done anymore.
49
+ - The new version of the Discord gateway (v4) is supported and used by default. This should bring more stability and possibly slight performance improvements.
50
+ - Some older v3 features that weren't supported before are now:
51
+ - Compressed ready packets (should decrease network overhead for very large bots)
52
+ - Discord rate limits are now supported better - the client will never send a message if it knows it's going to be rate limited, instead it's going to wait for the correct time.
53
+ - Requests will now automatically be retried if a 502 (cloudflare error) is received.
54
+ - `MessageEditEvent`s now have a whole message instead of just the ID to allow for checking the content of edited messages.
55
+ - `Message`s now have an `attachments` array with files attached to the message.
56
+ - `ReadyEvent` and `DisconnectEvent` now have the bot as a readable attribute - useful for container-based bots that don't have a way to get them otherwise.
57
+ - `Bot#find_channel` can now parse channel mentions and search for specific types of channels (text or voice).
58
+ - `Server#create_channel` can now create voice channels.
59
+ - A utility function `User#distinct` was added to get the distinct representation of a user (i.e. name + discrim, for example "meew0#9811")
60
+ - The `User#discriminator` attribute now has more aliases (`#tag`, `#discord_tag`, `#discrim`)
61
+ - `Permission` objects can now be created or set even without a role writer, useful to quickly get byte representations of permissions
62
+ - Permission overwrites can now be defined more easily using the utility method `Channel#define_overwrite`
63
+ - `Message`s returned at the end of commands (for example using `User#pm` or `Message#edit`) will now no longer be sent ([#66](https://github.com/meew0/discordrb/issues/66))
64
+ - The `:with_text` event attribute is now aliased to `:exact_text` ([#65](https://github.com/meew0/discordrb/issues/65))
65
+ - Server icons (`Server#icon=`) can now be set just like avatars (`Profile#avatar=`)
66
+ - Lots of comments were added to the examples and some bugs fixed
67
+ - The overall performance and memory usage was improved, especially on Ruby 2.3 (using the new frozen string literal comment)
68
+ - The documentation was slightly improved.
69
+
70
+ **Bugfixes**:
71
+ - A *lot* of latent bugs with caching were fixed. This doesn't really have a noticeable effect, it just means better stability and reliability as a whole.
72
+ - **Command bots no longer respond when there are spaces between the prefix and the command.** Because this behaviour may be desirable, a `spaces_allowed` attribute was added to the CommandBot initializer that can be set to true to re-enable this behaviour.
73
+ - Permission calculation (`User#permission?`) has been thoroughly rewritten and should now account for edge cases like server owners and Manage Permissions.
74
+ - The gateway reconnect logic now uses a correct falloff system - before it would start at 1 second between attempts and immediately jump to 120. Now the transition is more smooth.
75
+ - Commands with aliases now show up correctly in the auto-generated help command ([#72](https://github.com/meew0/discordrb/issues/72))
76
+ - The auto-generated help command can now actually be disabled by setting the corresponding attribute to nil ([#73](https://github.com/meew0/discordrb/issues/73))
77
+ - Including empty containers now does nothing instead of raising an error
78
+ - Command bots now obey `should_parse_self`
79
+
3
80
  ## 1.8.1
4
81
 
5
82
  ### Bugfixes
data/README.md CHANGED
@@ -1,18 +1,36 @@
1
+ [![Gem](https://img.shields.io/gem/v/discordrb.svg)](https://rubygems.org/gems/discordrb)
2
+ [![Gem](https://img.shields.io/gem/dt/discordrb.svg)](https://rubygems.org/gems/discordrb)
1
3
  [![Build Status](https://travis-ci.org/meew0/discordrb.svg?branch=master)](https://travis-ci.org/meew0/discordrb)
2
-
4
+ [![Inline docs](http://inch-ci.org/github/meew0/discordrb.svg?branch=master&style=shields)](http://inch-ci.org/github/meew0/discordrb)
5
+ [![Join Discord](https://img.shields.io/badge/discord-join-7289DA.svg)](https://discord.gg/0SBTUU1wZTWfFQL2)
3
6
  # discordrb
4
7
 
5
8
  An implementation of the [Discord](https://discordapp.com/) API using Ruby.
6
9
 
10
+ **News**: Please help test version 2 of discordrb with a lot of changes! A preliminary changelog can be found [here](https://gist.github.com/meew0/ea120051da52604e7873b7cfaed4c40b). The code can be found on the `v2` branch; if you're using bundler you can change the gem reference to `gem 'discordrb', git: 'git://github.com/meew0/discordrb.git', branch: 'v2'`. A downloadable gem file for everybody else will follow here shortly.
11
+
7
12
  ## Quick links to sections
8
13
 
14
+ * [Dependencies](https://github.com/meew0/discordrb#dependencies)
9
15
  * [Installation](https://github.com/meew0/discordrb#installation)
10
16
  * [Usage](https://github.com/meew0/discordrb#usage)
11
17
  * [Support](https://github.com/meew0/discordrb#support)
12
18
  * [Development](https://github.com/meew0/discordrb#development), [Contributing](https://github.com/meew0/discordrb#contributing)
13
19
  * [License](https://github.com/meew0/discordrb#license)
14
20
 
15
- See also: [Documentation](https://discord.gg/0SBTUU1wZTWfFQL2), [Tutorials](https://github.com/meew0/discordrb/wiki)
21
+ See also: [Documentation](http://www.rubydoc.info/gems/discordrb), [Tutorials](https://github.com/meew0/discordrb/wiki)
22
+
23
+ ## Dependencies
24
+
25
+ * Ruby 2.1+
26
+ * An installed build system for native extensions (on Windows, try the [DevKit](http://rubyinstaller.org/downloads/); installation instructions [here](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit#quick-start))
27
+
28
+ ### Voice dependencies
29
+
30
+ This section only applies to you if you want to use voice functionality.
31
+ * [libsodium](https://github.com/meew0/discordrb/wiki/Installing-libsodium)
32
+ * A compiled libopus distribution for your system, anywhere the script can find it (on Windows, make sure it's named `opus.dll`)
33
+ * [FFmpeg](https://www.ffmpeg.org/download.html) installed and in your PATH
16
34
 
17
35
  ## Installation
18
36
 
@@ -49,16 +67,6 @@ Then reinstall discordrb:
49
67
  gem uninstall discordrb
50
68
  gem install discordrb
51
69
 
52
- **If you get an error like this when running the example**:
53
-
54
- terminate called after throwing an instance of 'std::runtime_error'
55
- what(): Encryption not available on this event-machine
56
-
57
- You're missing the OpenSSL libraries that EventMachine, a dependency of discordrb, needs to be built with to use encrypted connections (which Discord requires). Download the OpenSSL libraries from [here](https://slproweb.com/download/Win32OpenSSL-1_0_2f.exe), install them to their default location and reinstall EventMachine using these libraries:
58
-
59
- gem uninstall eventmachine
60
- gem install eventmachine -- --with-ssl-dir=C:/OpenSSL-Win32
61
-
62
70
  **If you're having trouble getting voice playback to work**:
63
71
 
64
72
  Look here: https://github.com/meew0/discordrb/wiki/Voice-sending#troubleshooting
@@ -70,10 +78,10 @@ You can make a simple bot like this:
70
78
  ```ruby
71
79
  require 'discordrb'
72
80
 
73
- bot = Discordrb::Bot.new "email@example.com", "hunter2"
81
+ bot = Discordrb::Bot.new token: '<token here>'
74
82
 
75
- bot.message(with_text: "Ping!") do |event|
76
- event.respond "Pong!"
83
+ bot.message(with_text: 'Ping!') do |event|
84
+ event.respond 'Pong!'
77
85
  end
78
86
 
79
87
  bot.run
@@ -87,6 +95,8 @@ You can find me (@meew0, ID 66237334693085184) on the unofficial Discord API ser
87
95
 
88
96
  ## Development
89
97
 
98
+ **This section is for developing discordrb itself! If you just want to make a bot, see the [Installation](https://github.com/meew0/discordrb#installation) section.**
99
+
90
100
  After checking out the repo, run `bin/setup` to install dependencies. You can then run tests via `bundle exec rspec spec`. Make sure to run rubocop also: `bundle exec rubocop`. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
91
101
 
92
102
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
@@ -19,11 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.add_dependency 'faye-websocket'
23
22
  spec.add_dependency 'rest-client'
24
23
  spec.add_dependency 'activesupport'
25
24
  spec.add_dependency 'opus-ruby'
26
- spec.add_dependency 'websocket-client-simple'
25
+ spec.add_dependency 'websocket-client-simple', '>= 0.3.0'
27
26
  spec.add_dependency 'rbnacl'
28
27
 
29
28
  spec.required_ruby_version = '>= 2.1.0'
@@ -32,5 +31,5 @@ Gem::Specification.new do |spec|
32
31
  spec.add_development_dependency 'rake', '~> 10.0'
33
32
  spec.add_development_dependency 'yard', '~> 0.8.7.6'
34
33
  spec.add_development_dependency 'rspec', '~> 3.4.0'
35
- spec.add_development_dependency 'rubocop', '0.37.2'
34
+ spec.add_development_dependency 'rubocop', '0.39.0'
36
35
  end
@@ -2,25 +2,35 @@
2
2
 
3
3
  require 'discordrb'
4
4
 
5
- bot = Discordrb::Commands::CommandBot.new 'email@example.com', 'hunter2', '!'
5
+ bot = Discordrb::Commands::CommandBot.new token: 'B0T.T0KEN.here', application_id: 160123456789876543, prefix: '!'
6
6
 
7
7
  bot.command :user do |event|
8
+ # Commands send whatever is returned from the block to the channel. This allows for compact commands like this,
9
+ # but you have to be aware of this so you don't accidentally return something you didn't intend to.
10
+ # To prevent the return value to be sent to the channel, you can just return `nil`.
8
11
  event.user.name
9
12
  end
10
13
 
11
14
  bot.command :bold do |_event, *args|
15
+ # Again, the return value of the block is sent to the channel
12
16
  "**#{args.join(' ')}**"
13
17
  end
14
18
 
15
19
  bot.command :italic do |_event, *args|
16
- "**#{args.join(' ')}**"
20
+ "*#{args.join(' ')}*"
17
21
  end
18
22
 
19
23
  bot.command(:join, permission_level: 1, chain_usable: false) do |event, invite|
20
24
  event.bot.join invite
25
+
26
+ # The `join` call above returns the data Discord sends as a response to joining the server. We don't want that
27
+ # in the channel so here we're just returning `nil` afterwards
28
+ nil
21
29
  end
22
30
 
23
31
  bot.command(:random, min_args: 0, max_args: 2, description: 'Generates a random number between 0 and 1, 0 and max or min and max.', usage: 'random [min/max] [max]') do |_event, min, max|
32
+ # The `if` statement returns one of multiple different things based on the condition. Its return value
33
+ # is then returned from the block and sent to the channel
24
34
  if max
25
35
  rand(min.to_i..max.to_i)
26
36
  elsif min
@@ -35,6 +45,8 @@ bot.command :long do |event|
35
45
  event << 'It has multiple lines that are each sent by doing `event << line`.'
36
46
  event << 'This is an easy way to do such long messages, or to create lines that should only be sent conditionally.'
37
47
  event << 'Anyway, have a nice day.'
48
+
49
+ # Here we don't have to worry about the return value because the `event << line` statement automatically returns nil.
38
50
  end
39
51
 
40
52
  bot.run
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'discordrb'
4
4
 
5
- bot = Discordrb::Bot.new 'email@example.com', 'hunter2'
5
+ bot = Discordrb::Bot.new token: 'B0T.T0KEN.here', application_id: 160123456789876543
6
6
 
7
7
  bot.message(with_text: 'Ping!') do |event|
8
8
  event.respond 'Pong!'
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'discordrb'
4
4
 
5
- bot = Discordrb::Bot.new 'email@example.com', 'hunter2'
5
+ bot = Discordrb::Bot.new token: 'B0T.T0KEN.here', application_id: 160123456789876543
6
6
 
7
7
  bot.mention do |event|
8
8
  event.user.pm('You have mentioned me!')
@@ -1,3 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless ENV['DISCORDRB_V2_MESSAGE']
4
+ puts "You're using version 2 of discordrb which has some breaking changes!"
5
+ puts "Don't worry if your bot crashes, you can find a list and migration advice here:"
6
+ puts ' https://github.com/meew0/discordrb/blob/master/CHANGELOG.md#200'
7
+ puts 'This message will go away in version 2.1 or can be disabled by setting the DISCORDRB_V2_MESSAGE environment variable.'
8
+ end
9
+
1
10
  require 'discordrb/version'
2
11
  require 'discordrb/bot'
3
12
  require 'discordrb/commands/command_bot'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rest-client'
2
4
  require 'json'
3
5
 
@@ -10,6 +12,16 @@ module Discordrb::API
10
12
 
11
13
  module_function
12
14
 
15
+ # @return [String] the currently used API base URL.
16
+ def api_base
17
+ @api_base || APIBASE
18
+ end
19
+
20
+ # Sets the API base URL to something.
21
+ def api_base=(value)
22
+ @api_base = value
23
+ end
24
+
13
25
  # @return [String] the bot name, previously specified using #bot_name=.
14
26
  def bot_name
15
27
  @bot_name
@@ -29,6 +41,13 @@ module Discordrb::API
29
41
  "rest-client/#{RestClient::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} discordrb/#{Discordrb::VERSION} #{required} #{@bot_name}"
30
42
  end
31
43
 
44
+ # Resets all rate limit mutexes
45
+ def reset_mutexes
46
+ @mutexes = {
47
+ message: Mutex.new
48
+ }
49
+ end
50
+
32
51
  # Performs a RestClient request.
33
52
  # @param type [Symbol] The type of HTTP request to use.
34
53
  # @param attributes [Array] The attributes for the request.
@@ -36,20 +55,39 @@ module Discordrb::API
36
55
  RestClient.send(type, *attributes)
37
56
  rescue RestClient::Forbidden
38
57
  raise Discordrb::Errors::NoPermission, "The bot doesn't have the required permission to do this!"
58
+ rescue RestClient::BadGateway
59
+ Discordrb::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
60
+ retry
39
61
  end
40
62
 
41
63
  # Make an API request. Utility function to implement message queueing
42
64
  # in the future
43
- def request(type, *attributes)
65
+ def request(key, type, *attributes)
44
66
  # Add a custom user agent
45
67
  attributes.last[:user_agent] = user_agent if attributes.last.is_a? Hash
46
- response = raw_request(type, attributes)
47
68
 
48
- while response.code == 429
49
- wait_seconds = response[:retry_after].to_i / 1000.0
50
- LOGGER.debug("WARNING: Discord rate limiting will cause a delay of #{wait_seconds} seconds for the request: #{type} #{attributes}")
51
- sleep wait_seconds / 1000.0
69
+ begin
70
+ if key
71
+ # Lock and unlock, i. e. wait for the mutex to unlock and don't do anything with it afterwards
72
+ @mutexes[key].lock
73
+ @mutexes[key].unlock
74
+ end
75
+
52
76
  response = raw_request(type, attributes)
77
+ rescue RestClient::TooManyRequests => e
78
+ raise "Got an HTTP 429 for an untracked API call! Please report this bug together with the following information: #{type} #{attributes}" unless key
79
+
80
+ unless @mutexes[key].locked?
81
+ response = JSON.parse(e.response)
82
+ wait_seconds = response['retry_after'].to_i / 1000.0
83
+ Discordrb::LOGGER.warn("Locking RL mutex (key: #{key}) for #{wait_seconds} seconds due to Discord rate limiting")
84
+
85
+ # Wait the required time synchronized by the mutex (so other incoming requests have to wait) but only do it if
86
+ # the mutex isn't locked already so it will only ever wait once
87
+ @mutexes[key].synchronize { sleep wait_seconds }
88
+ end
89
+
90
+ retry
53
91
  end
54
92
 
55
93
  response
@@ -57,14 +95,20 @@ module Discordrb::API
57
95
 
58
96
  # Make an avatar URL from the user and avatar IDs
59
97
  def avatar_url(user_id, avatar_id)
60
- "#{APIBASE}/users/#{user_id}/avatars/#{avatar_id}.jpg"
98
+ "#{api_base}/users/#{user_id}/avatars/#{avatar_id}.jpg"
99
+ end
100
+
101
+ # Make an icon URL from server and icon IDs
102
+ def icon_url(server_id, icon_id)
103
+ "#{api_base}/guilds/#{server_id}/icons/#{icon_id}.jpg"
61
104
  end
62
105
 
63
106
  # Ban a user from a server and delete their messages from the last message_days days
64
107
  def ban_user(token, server_id, user_id, message_days)
65
108
  request(
109
+ nil,
66
110
  :put,
67
- "#{APIBASE}/guilds/#{server_id}/bans/#{user_id}?delete-message-days=#{message_days}",
111
+ "#{api_base}/guilds/#{server_id}/bans/#{user_id}?delete-message-days=#{message_days}",
68
112
  nil,
69
113
  Authorization: token
70
114
  )
@@ -73,8 +117,9 @@ module Discordrb::API
73
117
  # Unban a user from a server
74
118
  def unban_user(token, server_id, user_id)
75
119
  request(
120
+ nil,
76
121
  :delete,
77
- "#{APIBASE}/guilds/#{server_id}/bans/#{user_id}",
122
+ "#{api_base}/guilds/#{server_id}/bans/#{user_id}",
78
123
  Authorization: token
79
124
  )
80
125
  end
@@ -82,8 +127,9 @@ module Discordrb::API
82
127
  # Kick a user from a server
83
128
  def kick_user(token, server_id, user_id)
84
129
  request(
130
+ nil,
85
131
  :delete,
86
- "#{APIBASE}/guilds/#{server_id}/members/#{user_id}",
132
+ "#{api_base}/guilds/#{server_id}/members/#{user_id}",
87
133
  Authorization: token
88
134
  )
89
135
  end
@@ -91,8 +137,9 @@ module Discordrb::API
91
137
  # Move a user to a different voice channel
92
138
  def move_user(token, server_id, user_id, channel_id)
93
139
  request(
140
+ nil,
94
141
  :patch,
95
- "#{APIBASE}/guilds/#{server_id}/members/#{user_id}",
142
+ "#{api_base}/guilds/#{server_id}/members/#{user_id}",
96
143
  { channel_id: channel_id }.to_json,
97
144
  Authorization: token,
98
145
  content_type: :json
@@ -102,8 +149,9 @@ module Discordrb::API
102
149
  # Get a server's banned users
103
150
  def bans(token, server_id)
104
151
  request(
152
+ nil,
105
153
  :get,
106
- "#{APIBASE}/guilds/#{server_id}/bans",
154
+ "#{api_base}/guilds/#{server_id}/bans",
107
155
  Authorization: token
108
156
  )
109
157
  end
@@ -111,8 +159,9 @@ module Discordrb::API
111
159
  # Login to the server
112
160
  def login(email, password)
113
161
  request(
162
+ nil,
114
163
  :post,
115
- "#{APIBASE}/auth/login",
164
+ "#{api_base}/auth/login",
116
165
  email: email,
117
166
  password: password
118
167
  )
@@ -121,8 +170,9 @@ module Discordrb::API
121
170
  # Logout from the server
122
171
  def logout(token)
123
172
  request(
173
+ nil,
124
174
  :post,
125
- "#{APIBASE}/auth/logout",
175
+ "#{api_base}/auth/logout",
126
176
  nil,
127
177
  Authorization: token
128
178
  )
@@ -131,8 +181,9 @@ module Discordrb::API
131
181
  # Create an OAuth application
132
182
  def create_oauth_application(token, name, redirect_uris)
133
183
  request(
184
+ nil,
134
185
  :post,
135
- "#{APIBASE}/oauth2/applications",
186
+ "#{api_base}/oauth2/applications",
136
187
  { name: name, redirect_uris: redirect_uris }.to_json,
137
188
  Authorization: token,
138
189
  content_type: :json
@@ -142,8 +193,9 @@ module Discordrb::API
142
193
  # Change an OAuth application's properties
143
194
  def update_oauth_application(token, name, redirect_uris, description = '', icon = nil)
144
195
  request(
196
+ nil,
145
197
  :put,
146
- "#{APIBASE}/oauth2/applications",
198
+ "#{api_base}/oauth2/applications",
147
199
  { name: name, redirect_uris: redirect_uris, description: description, icon: icon }.to_json,
148
200
  Authorization: token,
149
201
  content_type: :json
@@ -153,8 +205,9 @@ module Discordrb::API
153
205
  # Create a server
154
206
  def create_server(token, name, region = :london)
155
207
  request(
208
+ nil,
156
209
  :post,
157
- "#{APIBASE}/guilds",
210
+ "#{api_base}/guilds",
158
211
  { name: name, region: region.to_s }.to_json,
159
212
  Authorization: token,
160
213
  content_type: :json
@@ -164,8 +217,9 @@ module Discordrb::API
164
217
  # Update a server
165
218
  def update_server(token, server_id, name, region, icon, afk_channel_id, afk_timeout)
166
219
  request(
220
+ nil,
167
221
  :patch,
168
- "#{APIBASE}/guilds/#{server_id}",
222
+ "#{api_base}/guilds/#{server_id}",
169
223
  { name: name, region: region, icon: icon, afk_channel_id: afk_channel_id, afk_timeout: afk_timeout }.to_json,
170
224
  Authorization: token,
171
225
  content_type: :json
@@ -175,8 +229,9 @@ module Discordrb::API
175
229
  # Transfer server ownership
176
230
  def transfer_ownership(token, server_id, user_id)
177
231
  request(
232
+ nil,
178
233
  :patch,
179
- "#{APIBASE}/guilds/#{server_id}",
234
+ "#{api_base}/guilds/#{server_id}",
180
235
  { owner_id: user_id }.to_json,
181
236
  Authorization: token,
182
237
  content_type: :json
@@ -186,8 +241,9 @@ module Discordrb::API
186
241
  # Delete a server
187
242
  def delete_server(token, server_id)
188
243
  request(
244
+ nil,
189
245
  :delete,
190
- "#{APIBASE}/guilds/#{server_id}",
246
+ "#{api_base}/guilds/#{server_id}",
191
247
  Authorization: token
192
248
  )
193
249
  end
@@ -195,8 +251,9 @@ module Discordrb::API
195
251
  # Leave a server
196
252
  def leave_server(token, server_id)
197
253
  request(
254
+ nil,
198
255
  :delete,
199
- "#{APIBASE}/users/@me/guilds/#{server_id}",
256
+ "#{api_base}/users/@me/guilds/#{server_id}",
200
257
  Authorization: token
201
258
  )
202
259
  end
@@ -204,8 +261,19 @@ module Discordrb::API
204
261
  # Get a channel's data
205
262
  def channel(token, channel_id)
206
263
  request(
264
+ nil,
207
265
  :get,
208
- "#{APIBASE}/channels/#{channel_id}",
266
+ "#{api_base}/channels/#{channel_id}",
267
+ Authorization: token
268
+ )
269
+ end
270
+
271
+ # Get a server's data
272
+ def server(token, server_id)
273
+ request(
274
+ nil,
275
+ :get,
276
+ "#{api_base}/guilds/#{server_id}",
209
277
  Authorization: token
210
278
  )
211
279
  end
@@ -213,8 +281,9 @@ module Discordrb::API
213
281
  # Get a member's data
214
282
  def member(token, server_id, user_id)
215
283
  request(
284
+ nil,
216
285
  :get,
217
- "#{APIBASE}/guilds/#{server_id}/members/#{user_id}",
286
+ "#{api_base}/guilds/#{server_id}/members/#{user_id}",
218
287
  Authorization: token
219
288
  )
220
289
  end
@@ -222,8 +291,9 @@ module Discordrb::API
222
291
  # Create a channel
223
292
  def create_channel(token, server_id, name, type)
224
293
  request(
294
+ nil,
225
295
  :post,
226
- "#{APIBASE}/guilds/#{server_id}/channels",
296
+ "#{api_base}/guilds/#{server_id}/channels",
227
297
  { name: name, type: type }.to_json,
228
298
  Authorization: token,
229
299
  content_type: :json
@@ -233,8 +303,9 @@ module Discordrb::API
233
303
  # Update a channel's data
234
304
  def update_channel(token, channel_id, name, topic, position = 0)
235
305
  request(
306
+ nil,
236
307
  :patch,
237
- "#{APIBASE}/channels/#{channel_id}",
308
+ "#{api_base}/channels/#{channel_id}",
238
309
  { name: name, position: position, topic: topic }.to_json,
239
310
  Authorization: token,
240
311
  content_type: :json
@@ -244,8 +315,9 @@ module Discordrb::API
244
315
  # Delete a channel
245
316
  def delete_channel(token, channel_id)
246
317
  request(
318
+ nil,
247
319
  :delete,
248
- "#{APIBASE}/channels/#{channel_id}",
320
+ "#{api_base}/channels/#{channel_id}",
249
321
  Authorization: token
250
322
  )
251
323
  end
@@ -253,8 +325,9 @@ module Discordrb::API
253
325
  # Join a server using an invite
254
326
  def join_server(token, invite_code)
255
327
  request(
328
+ nil,
256
329
  :post,
257
- "#{APIBASE}/invite/#{invite_code}",
330
+ "#{api_base}/invite/#{invite_code}",
258
331
  nil,
259
332
  Authorization: token
260
333
  )
@@ -263,8 +336,9 @@ module Discordrb::API
263
336
  # Resolve an invite
264
337
  def resolve_invite(token, invite_code)
265
338
  request(
339
+ nil,
266
340
  :get,
267
- "#{APIBASE}/invite/#{invite_code}",
341
+ "#{api_base}/invite/#{invite_code}",
268
342
  Authorization: token
269
343
  )
270
344
  end
@@ -272,19 +346,23 @@ module Discordrb::API
272
346
  # Create a private channel
273
347
  def create_private(token, bot_user_id, user_id)
274
348
  request(
349
+ nil,
275
350
  :post,
276
- "#{APIBASE}/users/#{bot_user_id}/channels",
351
+ "#{api_base}/users/#{bot_user_id}/channels",
277
352
  { recipient_id: user_id }.to_json,
278
353
  Authorization: token,
279
354
  content_type: :json
280
355
  )
356
+ rescue RestClient::BadRequest
357
+ raise 'Attempted to PM the bot itself!'
281
358
  end
282
359
 
283
360
  # Create an instant invite from a server or a channel id
284
361
  def create_invite(token, channel_id, max_age = 0, max_uses = 0, temporary = false, xkcd = false)
285
362
  request(
363
+ nil,
286
364
  :post,
287
- "#{APIBASE}/channels/#{channel_id}/invites",
365
+ "#{api_base}/channels/#{channel_id}/invites",
288
366
  { max_age: max_age, max_uses: max_uses, temporary: temporary, xkcdpass: xkcd }.to_json,
289
367
  Authorization: token,
290
368
  content_type: :json
@@ -294,8 +372,9 @@ module Discordrb::API
294
372
  # Delete an invite by code
295
373
  def delete_invite(token, code)
296
374
  request(
375
+ nil,
297
376
  :delete,
298
- "#{APIBASE}/invites/#{code}",
377
+ "#{api_base}/invites/#{code}",
299
378
  Authorization: token
300
379
  )
301
380
  end
@@ -303,23 +382,23 @@ module Discordrb::API
303
382
  # Send a message to a channel
304
383
  def send_message(token, channel_id, message, mentions = [], tts = false)
305
384
  request(
385
+ :message,
306
386
  :post,
307
- "#{APIBASE}/channels/#{channel_id}/messages",
387
+ "#{api_base}/channels/#{channel_id}/messages",
308
388
  { content: message, mentions: mentions, tts: tts }.to_json,
309
389
  Authorization: token,
310
390
  content_type: :json
311
391
  )
312
392
  rescue RestClient::InternalServerError
313
393
  raise Discordrb::Errors::MessageTooLong, "Message over the character limit (#{message.length} > 2000)"
314
- rescue RestClient::BadGateway
315
- raise Discordrb::Errors::CloudflareError, "Discord's Cloudflare system encountered an error! Usually you can ignore this error and retry the request."
316
394
  end
317
395
 
318
396
  # Delete a message
319
397
  def delete_message(token, channel_id, message_id)
320
398
  request(
399
+ nil,
321
400
  :delete,
322
- "#{APIBASE}/channels/#{channel_id}/messages/#{message_id}",
401
+ "#{api_base}/channels/#{channel_id}/messages/#{message_id}",
323
402
  Authorization: token
324
403
  )
325
404
  end
@@ -327,8 +406,9 @@ module Discordrb::API
327
406
  # Edit a message
328
407
  def edit_message(token, channel_id, message_id, message, mentions = [])
329
408
  request(
409
+ :message,
330
410
  :patch,
331
- "#{APIBASE}/channels/#{channel_id}/messages/#{message_id}",
411
+ "#{api_base}/channels/#{channel_id}/messages/#{message_id}",
332
412
  { content: message, mentions: mentions }.to_json,
333
413
  Authorization: token,
334
414
  content_type: :json
@@ -340,8 +420,9 @@ module Discordrb::API
340
420
  # so this is an easy way to catch up on messages
341
421
  def acknowledge_message(token, channel_id, message_id)
342
422
  request(
423
+ nil,
343
424
  :post,
344
- "#{APIBASE}/channels/#{channel_id}/messages/#{message_id}/ack",
425
+ "#{api_base}/channels/#{channel_id}/messages/#{message_id}/ack",
345
426
  nil,
346
427
  Authorization: token
347
428
  )
@@ -350,8 +431,9 @@ module Discordrb::API
350
431
  # Send a file as a message to a channel
351
432
  def send_file(token, channel_id, file)
352
433
  request(
434
+ nil,
353
435
  :post,
354
- "#{APIBASE}/channels/#{channel_id}/messages",
436
+ "#{api_base}/channels/#{channel_id}/messages",
355
437
  { file: file },
356
438
  Authorization: token
357
439
  )
@@ -360,8 +442,9 @@ module Discordrb::API
360
442
  # Create a role (parameters such as name and colour will have to be set by update_role afterwards)
361
443
  def create_role(token, server_id)
362
444
  request(
445
+ nil,
363
446
  :post,
364
- "#{APIBASE}/guilds/#{server_id}/roles",
447
+ "#{api_base}/guilds/#{server_id}/roles",
365
448
  nil,
366
449
  Authorization: token
367
450
  )
@@ -373,8 +456,9 @@ module Discordrb::API
373
456
  # connecting to voice, speaking and voice activity (push-to-talk isn't mandatory)
374
457
  def update_role(token, server_id, role_id, name, colour, hoist = false, packed_permissions = 36_953_089)
375
458
  request(
459
+ nil,
376
460
  :patch,
377
- "#{APIBASE}/guilds/#{server_id}/roles/#{role_id}",
461
+ "#{api_base}/guilds/#{server_id}/roles/#{role_id}",
378
462
  { color: colour, name: name, hoist: hoist, permissions: packed_permissions }.to_json,
379
463
  Authorization: token,
380
464
  content_type: :json
@@ -384,8 +468,9 @@ module Discordrb::API
384
468
  # Delete a role
385
469
  def delete_role(token, server_id, role_id)
386
470
  request(
471
+ nil,
387
472
  :delete,
388
- "#{APIBASE}/guilds/#{server_id}/roles/#{role_id}",
473
+ "#{api_base}/guilds/#{server_id}/roles/#{role_id}",
389
474
  Authorization: token
390
475
  )
391
476
  end
@@ -393,8 +478,9 @@ module Discordrb::API
393
478
  # Update a user's roles
394
479
  def update_user_roles(token, server_id, user_id, roles)
395
480
  request(
481
+ nil,
396
482
  :patch,
397
- "#{APIBASE}/guilds/#{server_id}/members/#{user_id}",
483
+ "#{api_base}/guilds/#{server_id}/members/#{user_id}",
398
484
  { roles: roles }.to_json,
399
485
  Authorization: token,
400
486
  content_type: :json
@@ -404,8 +490,9 @@ module Discordrb::API
404
490
  # Update a user's permission overrides in a channel
405
491
  def update_user_overrides(token, channel_id, user_id, allow, deny)
406
492
  request(
493
+ nil,
407
494
  :put,
408
- "#{APIBASE}/channels/#{channel_id}/permissions/#{user_id}",
495
+ "#{api_base}/channels/#{channel_id}/permissions/#{user_id}",
409
496
  { type: 'member', id: user_id, allow: allow, deny: deny }.to_json,
410
497
  Authorization: token,
411
498
  content_type: :json
@@ -415,8 +502,9 @@ module Discordrb::API
415
502
  # Update a role's permission overrides in a channel
416
503
  def update_role_overrides(token, channel_id, role_id, allow, deny)
417
504
  request(
505
+ nil,
418
506
  :put,
419
- "#{APIBASE}/channels/#{channel_id}/permissions/#{role_id}",
507
+ "#{api_base}/channels/#{channel_id}/permissions/#{role_id}",
420
508
  { type: 'role', id: role_id, allow: allow, deny: deny }.to_json,
421
509
  Authorization: token,
422
510
  content_type: :json
@@ -426,8 +514,9 @@ module Discordrb::API
426
514
  # Get the gateway to be used
427
515
  def gateway(token)
428
516
  request(
517
+ nil,
429
518
  :get,
430
- "#{APIBASE}/gateway",
519
+ "#{api_base}/gateway",
431
520
  Authorization: token
432
521
  )
433
522
  end
@@ -435,8 +524,9 @@ module Discordrb::API
435
524
  # Validate a token (this request will fail if the token is invalid)
436
525
  def validate_token(token)
437
526
  request(
527
+ nil,
438
528
  :post,
439
- "#{APIBASE}/auth/login",
529
+ "#{api_base}/auth/login",
440
530
  {}.to_json,
441
531
  Authorization: token,
442
532
  content_type: :json
@@ -446,8 +536,9 @@ module Discordrb::API
446
536
  # Start typing (needs to be resent every 5 seconds to keep up the typing)
447
537
  def start_typing(token, channel_id)
448
538
  request(
539
+ nil,
449
540
  :post,
450
- "#{APIBASE}/channels/#{channel_id}/typing",
541
+ "#{api_base}/channels/#{channel_id}/typing",
451
542
  nil,
452
543
  Authorization: token
453
544
  )
@@ -456,8 +547,29 @@ module Discordrb::API
456
547
  # Get user data
457
548
  def user(token, user_id)
458
549
  request(
550
+ nil,
459
551
  :get,
460
- "#{APIBASE}/users/#{user_id}",
552
+ "#{api_base}/users/#{user_id}",
553
+ Authorization: token
554
+ )
555
+ end
556
+
557
+ # Get profile data
558
+ def profile(token)
559
+ request(
560
+ nil,
561
+ :get,
562
+ "#{api_base}/users/@me",
563
+ Authorization: token
564
+ )
565
+ end
566
+
567
+ # Get information about a user's connections
568
+ def connections(token)
569
+ request(
570
+ nil,
571
+ :get,
572
+ "#{api_base}/users/@me/connections",
461
573
  Authorization: token
462
574
  )
463
575
  end
@@ -465,20 +577,34 @@ module Discordrb::API
465
577
  # Update user data
466
578
  def update_user(token, email, password, new_username, avatar, new_password = nil)
467
579
  request(
580
+ nil,
468
581
  :patch,
469
- "#{APIBASE}/users/@me",
582
+ "#{api_base}/users/@me",
470
583
  { avatar: avatar, email: email, new_password: new_password, password: password, username: new_username }.to_json,
471
584
  Authorization: token,
472
585
  content_type: :json
473
586
  )
474
587
  end
475
588
 
589
+ # Get the servers a user is connected to
590
+ def servers(token)
591
+ request(
592
+ nil,
593
+ :get,
594
+ "#{api_base}/users/@me/guilds",
595
+ Authorization: token
596
+ )
597
+ end
598
+
476
599
  # Get a list of messages from a channel's history
477
600
  def channel_log(token, channel_id, amount, before = nil, after = nil)
478
601
  request(
602
+ nil,
479
603
  :get,
480
- "#{APIBASE}/channels/#{channel_id}/messages?limit=#{amount}#{"&before=#{before}" if before}#{"&after=#{after}" if after}",
604
+ "#{api_base}/channels/#{channel_id}/messages?limit=#{amount}#{"&before=#{before}" if before}#{"&after=#{after}" if after}",
481
605
  Authorization: token
482
606
  )
483
607
  end
484
608
  end
609
+
610
+ Discordrb::API.reset_mutexes