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
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'discordrb/events/generic'
4
+ require 'discordrb/data'
5
+
6
+ module Discordrb::Events
7
+ # Raised when a role is created on a server
8
+ class ServerRoleCreateEvent < Event
9
+ # @return [Role] the role that got created
10
+ attr_reader :role
11
+
12
+ # @return [Server] the server on which a role got created
13
+ attr_reader :server
14
+
15
+ def initialize(data, bot)
16
+ @server = bot.server(data['guild_id'].to_i)
17
+ return unless @server
18
+
19
+ role_id = data['role']['id'].to_i
20
+ @role = @server.roles.find { |r| r.id == role_id }
21
+ end
22
+ end
23
+
24
+ # Event handler for ServerRoleCreateEvent
25
+ class ServerRoleCreateEventHandler < EventHandler
26
+ def matches?(event)
27
+ # Check for the proper event type
28
+ return false unless event.is_a? ServerRoleCreateEvent
29
+
30
+ [
31
+ matches_all(@attributes[:name], event.name) do |a, e|
32
+ a == if a.is_a? String
33
+ e.to_s
34
+ else
35
+ e
36
+ end
37
+ end
38
+ ].reduce(true, &:&)
39
+ end
40
+ end
41
+
42
+ # Raised when a role is deleted from a server
43
+ class ServerRoleDeleteEvent < Event
44
+ # @return [Integer] the ID of the role that got deleted.
45
+ attr_reader :id
46
+
47
+ # @return [Server] the server on which a role got deleted.
48
+ attr_reader :server
49
+
50
+ def initialize(data, bot)
51
+ # The role should already be deleted from the server's list
52
+ # by the time we create this event, so we'll create a temporary
53
+ # role object for event consumers to use.
54
+ @id = data['role_id'].to_i
55
+ server_id = data['guild_id'].to_i
56
+ @server = bot.server(server_id)
57
+ end
58
+ end
59
+
60
+ # EventHandler for ServerRoleDeleteEvent
61
+ class ServerRoleDeleteEventHandler < EventHandler
62
+ def matches?(event)
63
+ # Check for the proper event type
64
+ return false unless event.is_a? ServerRoleDeleteEvent
65
+
66
+ [
67
+ matches_all(@attributes[:name], event.name) do |a, e|
68
+ a == if a.is_a? String
69
+ e.to_s
70
+ else
71
+ e
72
+ end
73
+ end
74
+ ].reduce(true, &:&)
75
+ end
76
+ end
77
+
78
+ # Event raised when a role updates on a server
79
+ class ServerRoleUpdateEvent < ServerRoleCreateEvent; end
80
+
81
+ # Event handler for ServerRoleUpdateEvent
82
+ class ServerRoleUpdateEventHandler < ServerRoleCreateEventHandler; end
83
+ end
@@ -1,15 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'discordrb/events/generic'
2
4
 
3
5
  module Discordrb::Events
4
6
  # Event raised when a user starts typing
5
7
  class TypingEvent < Event
6
- attr_reader :channel, :user, :timestamp
8
+ # @return [Channel] the channel on which a user started typing.
9
+ attr_reader :channel
10
+
11
+ # @return [Member] the user that started typing.
12
+ attr_reader :user
13
+ alias_method :member, :user
14
+
15
+ # @return [Time] when the typing happened.
16
+ attr_reader :timestamp
7
17
 
8
18
  def initialize(data, bot)
9
19
  @user_id = data['user_id'].to_i
10
- @user = bot.user(@user_id)
20
+
11
21
  @channel_id = data['channel_id'].to_i
12
22
  @channel = bot.channel(@channel_id)
23
+
24
+ @user = channel.private? ? channel.recipient : bot.member(@channel.server.id, @user_id)
25
+
13
26
  @timestamp = Time.at(data['timestamp'].to_i)
14
27
  end
15
28
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'discordrb/events/generic'
2
4
  require 'discordrb/data'
3
5
 
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'discordrb/light/light_bot'
4
+
5
+ # This module contains classes to allow connections to bots without a connection to the gateway socket, i. e. bots
6
+ # that only use the REST part of the API.
7
+ module Discordrb::Light
8
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'discordrb/data'
4
+
5
+ module Discordrb::Light
6
+ # Represents the bot account used for the light bot, but without any methods to change anything.
7
+ class LightProfile
8
+ include Discordrb::IDObject
9
+ include Discordrb::UserAttributes
10
+
11
+ # @!visibility private
12
+ def initialize(data, bot)
13
+ @bot = bot
14
+
15
+ @username = data['username']
16
+ @id = data['id'].to_i
17
+ @discriminator = data['discriminator']
18
+ @avatar_id = data['avatar']
19
+
20
+ @bot_account = false
21
+ @bot_account = true if data['bot']
22
+
23
+ @verified = data['verified']
24
+
25
+ @email = data['email']
26
+ end
27
+ end
28
+
29
+ # A server that only has an icon, a name, and an ID associated with it, like for example an integration's server.
30
+ class UltraLightServer
31
+ include Discordrb::IDObject
32
+ include Discordrb::ServerAttributes
33
+
34
+ # @!visibility private
35
+ def initialize(data, bot)
36
+ @bot = bot
37
+
38
+ @id = data['id'].to_i
39
+
40
+ @name = data['name']
41
+ @icon_id = data['icon']
42
+ end
43
+ end
44
+
45
+ # Represents a light server which only has a fraction of the properties of any other server.
46
+ class LightServer < UltraLightServer
47
+ # @return [true, false] whether or not the LightBot this server belongs to is the owner of the server.
48
+ attr_reader :bot_is_owner
49
+ alias_method :bot_is_owner?, :bot_is_owner
50
+
51
+ # @return [Discordrb::Permissions] the permissions the LightBot has on this server
52
+ attr_reader :bot_permissions
53
+
54
+ # @!visibility private
55
+ def initialize(data, bot)
56
+ super(data, bot)
57
+
58
+ @bot_is_owner = data['owner']
59
+ @bot_permissions = Discordrb::Permissions.new(data['permissions'])
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'discordrb/data'
4
+ require 'discordrb/light/data'
5
+
6
+ module Discordrb::Light
7
+ # A connection of your Discord account to a particular other service (currently, Twitch and YouTube)
8
+ class Connection
9
+ # @return [Symbol] what type of connection this is (either :twitch or :youtube currently)
10
+ attr_reader :type
11
+
12
+ # @return [true, false] whether this connection is revoked
13
+ attr_reader :revoked
14
+ alias_method :revoked?, :revoked
15
+
16
+ # @return [String] the name of the connected account
17
+ attr_reader :name
18
+
19
+ # @return [String] the ID of the connected account
20
+ attr_reader :id
21
+
22
+ # @return [Array<Integration>] the integrations associated with this connection
23
+ attr_reader :integrations
24
+
25
+ # @!visibility private
26
+ def initialize(data, bot)
27
+ @bot = bot
28
+
29
+ @revoked = data['revoked']
30
+ @type = data['type'].to_sym
31
+ @name = data['name']
32
+ @id = data['id']
33
+
34
+ @integrations = data['integrations'].map { |e| Integration.new(e, self, bot) }
35
+ end
36
+ end
37
+
38
+ # An integration of a connection into a particular server, for example being a member of a subscriber-only Twitch
39
+ # server.
40
+ class Integration
41
+ include Discordrb::IDObject
42
+
43
+ # @return [UltraLightServer] the server associated with this integration
44
+ attr_reader :server
45
+
46
+ # @note The connection returned by this method will have no integrations itself, as Discord doesn't provide that
47
+ # data. Also, it will always be considered not revoked.
48
+ # @return [Connection] the server's underlying connection (for a Twitch subscriber-only server, it would be the
49
+ # Twitch account connection of the server owner).
50
+ attr_reader :server_connection
51
+
52
+ # @return [Connection] the connection integrated with the server (i. e. your connection)
53
+ attr_reader :integrated_connection
54
+
55
+ # @!visibility private
56
+ def initialize(data, integrated, bot)
57
+ @bot = bot
58
+ @integrated_connection = integrated
59
+
60
+ @server = UltraLightServer.new(data['guild'], bot)
61
+
62
+ # Restructure the given data so we can reuse the Connection initializer
63
+ restructured = {}
64
+
65
+ restructured['type'] = data['type']
66
+ restructured['id'] = data['account']['id']
67
+ restructured['name'] = data['account']['name']
68
+ restructured['integrations'] = []
69
+
70
+ @server_connection = Connection.new(restructured, bot)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'discordrb/api'
4
+ require 'discordrb/light/data'
5
+ require 'discordrb/light/integrations'
6
+
7
+ # This module contains classes to allow connections to bots without a connection to the gateway socket, i. e. bots
8
+ # that only use the REST part of the API.
9
+ module Discordrb::Light
10
+ # A bot that only uses the REST part of the API. Hierarchically unrelated to the regular {Discordrb::Bot}. Useful to
11
+ # make applications integrated to Discord over OAuth, for example.
12
+ class LightBot
13
+ # Create a new LightBot. This does no networking yet, all networking is done by the methods on this class.
14
+ # @param token [String] The token that should be used to authenticate to Discord. Can be an OAuth token or a regular
15
+ # user account token.
16
+ def initialize(token)
17
+ if token.respond_to? :token
18
+ # Parse AccessTokens from the OAuth2 gem
19
+ token = token.token
20
+ end
21
+
22
+ unless token.include? '.'
23
+ # Discord user/bot tokens always contain two dots, so if there's none we can assume it's an OAuth token.
24
+ token = "Bearer #{token}" # OAuth tokens have to be prefixed with 'Bearer' for Discord to be able to use them
25
+ end
26
+
27
+ @token = token
28
+ end
29
+
30
+ # @return [LightProfile] the details of the user this bot is connected to.
31
+ def profile
32
+ response = Discordrb::API.profile(@token)
33
+ LightProfile.new(JSON.parse(response), self)
34
+ end
35
+
36
+ # @return [Array<LightServer>] the servers this bot is connected to.
37
+ def servers
38
+ response = Discordrb::API.servers(@token)
39
+ JSON.parse(response).map { |e| LightServer.new(e, self) }
40
+ end
41
+
42
+ # Joins a server using an instant invite.
43
+ # @param code [String] The code part of the invite (for example 0cDvIgU2voWn4BaD if the invite URL is
44
+ # https://discord.gg/0cDvIgU2voWn4BaD)
45
+ def join(code)
46
+ Discordrb::API.join_server(@token, code)
47
+ end
48
+
49
+ # Gets the connections associated with this account.
50
+ # @return [Array<Connection>] this account's connections.
51
+ def connections
52
+ response = Discordrb::API.connections(@token)
53
+ JSON.parse(response).map { |e| Connection.new(e, self) }
54
+ end
55
+ end
56
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Discordrb
2
4
  # The format log timestamps should be in, in strftime format
3
5
  LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'.freeze
@@ -7,6 +9,8 @@ module Discordrb
7
9
  # @return [true, false] whether this logger is in extra-fancy mode!
8
10
  attr_writer :fancy
9
11
 
12
+ # Creates a new logger.
13
+ # @param fancy [true, false] Whether this logger uses fancy mode (ANSI escape codes to make the output colourful)
10
14
  def initialize(fancy = false)
11
15
  @fancy = fancy
12
16
  self.mode = :normal
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Discordrb
2
4
  # List of permissions Discord uses
3
5
  class Permissions
@@ -8,7 +10,7 @@ module Discordrb
8
10
  0 => :create_instant_invite, # 1
9
11
  1 => :kick_members, # 2
10
12
  2 => :ban_members, # 4
11
- 3 => :manage_roles, # 8
13
+ 3 => :manage_roles, # 8, also Manage Permissions
12
14
  4 => :manage_channels, # 16
13
15
  5 => :manage_server, # 32
14
16
  # 6 # 64
@@ -36,17 +38,15 @@ module Discordrb
36
38
  Flags.each do |position, flag|
37
39
  attr_reader flag
38
40
  define_method "can_#{flag}=" do |value|
39
- if @writer
40
- new_bits = @bits
41
- if value
42
- new_bits |= (1 << position)
43
- else
44
- new_bits &= ~(1 << position)
45
- end
46
- @writer.write(new_bits)
47
- @bits = new_bits
48
- init_vars
41
+ new_bits = @bits
42
+ if value
43
+ new_bits |= (1 << position)
44
+ else
45
+ new_bits &= ~(1 << position)
49
46
  end
47
+ @writer.write(new_bits) if @writer
48
+ @bits = new_bits
49
+ init_vars
50
50
  end
51
51
  end
52
52
 
@@ -67,7 +67,11 @@ module Discordrb
67
67
  end
68
68
  end
69
69
 
70
- def initialize(bits, writer = nil)
70
+ # Create a new Permissions object either as a blank slate to add permissions to (for example for
71
+ # {Channel#define_overwrite}) or from existing bit data to read out.
72
+ # @param bits [Integer] The permission bits that should be set from the beginning.
73
+ # @param writer [RoleWriter] The writer that should be used to update data when a permission is set.
74
+ def initialize(bits = 0, writer = nil)
71
75
  @writer = writer
72
76
  @bits = bits
73
77
  init_vars
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
  require 'json'
3
5
  require 'openssl'
@@ -10,6 +12,7 @@ module Discordrb
10
12
 
11
13
  # Represents a cached token with encryption data
12
14
  class CachedToken
15
+ # Parse the cached token from the JSON data read from the file.
13
16
  def initialize(data = nil)
14
17
  if data
15
18
  @verify_salt = Base64.decode64(data['verify_salt'])
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Discordrb and all its functionality, in this case only the version.
2
4
  module Discordrb
3
5
  # The current version of discordrb.
4
- VERSION = '1.8.1'.freeze
6
+ VERSION = '2.0.0'.freeze
5
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This makes opus an optional dependency
2
4
  begin
3
5
  require 'opus-ruby'
@@ -1,15 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'websocket-client-simple'
2
4
  require 'resolv'
3
5
  require 'socket'
4
6
  require 'json'
5
7
 
8
+ require 'discordrb/websocket'
9
+
6
10
  begin
7
- require 'rbnacl'
8
- RBNACL_AVAILABLE = true
11
+ RBNACL_AVAILABLE = false
12
+ unless ENV.key?('DISCORDRB_NONACL')
13
+ require 'rbnacl'
14
+ RBNACL_AVAILABLE = true
15
+ end
9
16
  rescue LoadError
10
17
  puts "libsodium not available! You can continue to use discordrb as normal but voice support won't work.
11
18
  Read https://github.com/meew0/discordrb/wiki/Installing-libsodium for more details."
12
- RBNACL_AVAILABLE = false
13
19
  end
14
20
 
15
21
  module Discordrb::Voice
@@ -42,7 +48,7 @@ module Discordrb::Voice
42
48
  def connect(endpoint, port, ssrc)
43
49
  @endpoint = endpoint
44
50
  @endpoint = @endpoint[6..-1] if @endpoint.start_with? 'wss://'
45
- @endpoint.gsub!(':80', '') # The endpoint may contain a port, we don't want that
51
+ @endpoint = @endpoint.gsub(':80', '') # The endpoint may contain a port, we don't want that
46
52
  @endpoint = Resolv.getaddress @endpoint
47
53
 
48
54
  @port = port
@@ -126,8 +132,7 @@ module Discordrb::Voice
126
132
  @token = token
127
133
  @session = session
128
134
 
129
- @endpoint = endpoint
130
- @endpoint.gsub!(':80', '')
135
+ @endpoint = endpoint.gsub(':80', '')
131
136
 
132
137
  @udp = VoiceUDP.new
133
138
  end
@@ -198,7 +203,7 @@ module Discordrb::Voice
198
203
  Thread.current[:discordrb_name] = 'vws-i'
199
204
 
200
205
  # Send the init packet
201
- send_init(@channel.server.id, @bot.bot_user.id, @session, @token)
206
+ send_init(@channel.server.id, @bot.profile.id, @session, @token)
202
207
  end
203
208
 
204
209
  # @!visibility private
@@ -285,15 +290,17 @@ module Discordrb::Voice
285
290
  def init_ws
286
291
  host = "wss://#{@endpoint}:443"
287
292
  @bot.debug("Connecting VWS to host: #{host}")
288
- @client = WebSocket::Client::Simple.connect(host)
289
293
 
290
- # Change some instance to local variables for the blocks
291
- instance = self
294
+ # Connect the WS
295
+ @client = Discordrb::WebSocket.new(
296
+ host,
297
+ method(:websocket_open),
298
+ method(:websocket_message),
299
+ proc { |e| puts "VWS error: #{e}" },
300
+ proc { |e| puts "VWS close: #{e}" }
301
+ )
292
302
 
293
- @client.on(:open) { instance.websocket_open }
294
- @client.on(:message) { |msg| instance.websocket_message(msg.data) }
295
- @client.on(:error) { |e| puts "VWS error: #{e}" }
296
- @client.on(:close) { |e| puts "VWS close: #{e}" }
303
+ @bot.debug('VWS connected')
297
304
 
298
305
  # Block any further execution
299
306
  heartbeat_loop