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,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'discordrb/api'
4
+ require 'discordrb/data'
5
+
6
+ module Discordrb
7
+ # This mixin module does caching stuff for the library. It conveniently separates the logic behind
8
+ # the caching (like, storing the user hashes or making API calls to retrieve things) from the Bot that
9
+ # actually uses it.
10
+ module Cache
11
+ # Initializes this cache
12
+ def init_cache
13
+ @users = {}
14
+
15
+ @servers = {}
16
+
17
+ @channels = {}
18
+ @private_channels = {}
19
+
20
+ @restricted_channels = []
21
+ end
22
+
23
+ # Gets a channel given its ID. This queries the internal channel cache, and if the channel doesn't
24
+ # exist in there, it will get the data from Discord.
25
+ # @param id [Integer] The channel ID for which to search for.
26
+ # @param server [Server] The server for which to search the channel for. If this isn't specified, it will be
27
+ # inferred using the API
28
+ # @return [Channel] The channel identified by the ID.
29
+ def channel(id, server = nil)
30
+ id = id.resolve_id
31
+
32
+ raise Discordrb::Errors::NoPermission if @restricted_channels.include? id
33
+
34
+ debug("Obtaining data for channel with id #{id}")
35
+ return @channels[id] if @channels[id]
36
+
37
+ begin
38
+ response = API.channel(token, id)
39
+ channel = Channel.new(JSON.parse(response), self, server)
40
+ @channels[id] = channel
41
+ rescue Discordrb::Errors::NoPermission
42
+ debug "Tried to get access to restricted channel #{id}, blacklisting it"
43
+ @restricted_channels << id
44
+ raise
45
+ end
46
+ end
47
+
48
+ # Gets a user by its ID.
49
+ # @note This can only resolve users known by the bot (i.e. that share a server with the bot).
50
+ # @param id [Integer] The user ID that should be resolved.
51
+ # @return [User, nil] The user identified by the ID, or `nil` if it couldn't be found.
52
+ def user(id)
53
+ id = id.resolve_id
54
+ return @users[id] if @users[id]
55
+
56
+ LOGGER.out("Resolving user #{id}")
57
+ response = API.user(token, id)
58
+ user = User.new(JSON.parse(response), self)
59
+ @users[id] = user
60
+ end
61
+
62
+ # Gets a server by its ID.
63
+ # @note This can only resolve servers the bot is currently in.
64
+ # @param id [Integer] The server ID that should be resolved.
65
+ # @return [Server, nil] The server identified by the ID, or `nil` if it couldn't be found.
66
+ def server(id)
67
+ id = id.resolve_id
68
+ return @servers[id] if @servers[id]
69
+
70
+ LOGGER.out("Resolving server #{id}")
71
+ response = API.server(token, id)
72
+ server = Server.new(JSON.parse(response), self)
73
+ @servers[id] = server
74
+ end
75
+
76
+ # Gets a member by both IDs
77
+ # @param server_id [Integer] The server ID for which a member should be resolved
78
+ # @param user_id [Integer] The ID of the user that should be resolved
79
+ # @return [Member, nil] The member identified by the IDs, or `nil` if none could be found
80
+ def member(server_id, user_id)
81
+ server_id = server_id.resolve_id
82
+ user_id = user_id.resolve_id
83
+
84
+ server = self.server(server_id)
85
+ return server.member(user_id) if server.member_cached?(user_id)
86
+
87
+ LOGGER.out("Resolving member #{server_id} on server #{user_id}")
88
+ response = API.member(token, server_id, user_id)
89
+ member = Member.new(JSON.parse(response), server, self)
90
+ server.cache_member(member)
91
+ end
92
+
93
+ # Creates a private channel for the given user ID, or if one exists already, returns that one.
94
+ # It is recommended that you use {User#pm} instead, as this is mainly for internal use. However,
95
+ # usage of this method may be unavoidable if only the user ID is known.
96
+ # @param id [Integer] The user ID to generate a private channel for.
97
+ # @return [Channel] A private channel for that user.
98
+ def private_channel(id)
99
+ id = id.resolve_id
100
+ debug("Creating private channel with user id #{id}")
101
+ return @private_channels[id] if @private_channels[id]
102
+
103
+ response = API.create_private(token, @profile.id, id)
104
+ channel = Channel.new(JSON.parse(response), self)
105
+ @private_channels[id] = channel
106
+ end
107
+
108
+ # Ensures a given user object is cached and if not, cache it from the given data hash.
109
+ # @param data [Hash] A data hash representing a user.
110
+ # @return [User] the user represented by the data hash.
111
+ def ensure_user(data)
112
+ if @users.include?(data['id'].to_i)
113
+ @users[data['id'].to_i]
114
+ else
115
+ @users[data['id'].to_i] = User.new(data, self)
116
+ end
117
+ end
118
+
119
+ # Ensures a given server object is cached and if not, cache it from the given data hash.
120
+ # @param data [Hash] A data hash representing a server.
121
+ # @return [Server] the server represented by the data hash.
122
+ def ensure_server(data)
123
+ if @servers.include?(data['id'].to_i)
124
+ @servers[data['id'].to_i]
125
+ else
126
+ @servers[data['id'].to_i] = Server.new(data, self)
127
+ end
128
+ end
129
+
130
+ # Ensures a given channel object is cached and if not, cache it from the given data hash.
131
+ # @param data [Hash] A data hash representing a channel.
132
+ # @return [Channel] the channel represented by the data hash.
133
+ def ensure_channel(data)
134
+ if @channels.include?(data['id'].to_i)
135
+ @channels[data['id'].to_i]
136
+ else
137
+ @channels[data['id'].to_i] = Channel.new(data, self)
138
+ end
139
+ end
140
+
141
+ # Requests member chunks for a given server ID.
142
+ # @param id [Integer] The server ID to request chunks for.
143
+ def request_chunks(id)
144
+ chunk_packet = {
145
+ op: 8,
146
+ d: {
147
+ guild_id: id,
148
+ query: '',
149
+ limit: 0
150
+ }
151
+ }.to_json
152
+ @ws.send(chunk_packet)
153
+ end
154
+
155
+ # Gets the code for an invite.
156
+ # @param invite [String, Invite] The invite to get the code for. Possible formats are:
157
+ #
158
+ # * An {Invite} object
159
+ # * The code for an invite
160
+ # * A fully qualified invite URL (e. g. `https://discordapp.com/invite/0A37aN7fasF7n83q`)
161
+ # * A short invite URL with protocol (e. g. `https://discord.gg/0A37aN7fasF7n83q`)
162
+ # * A short invite URL without protocol (e. g. `discord.gg/0A37aN7fasF7n83q`)
163
+ # @return [String] Only the code for the invite.
164
+ def resolve_invite_code(invite)
165
+ invite = invite.code if invite.is_a? Discordrb::Invite
166
+ invite = invite[invite.rindex('/') + 1..-1] if invite.start_with?('http', 'discord.gg')
167
+ invite
168
+ end
169
+
170
+ # Gets information about an invite.
171
+ # @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
172
+ # @return [Invite] The invite with information about the given invite URL.
173
+ def invite(invite)
174
+ code = resolve_invite_code(invite)
175
+ Invite.new(JSON.parse(API.resolve_invite(token, code)), self)
176
+ end
177
+
178
+ # Finds a channel given its name and optionally the name of the server it is in.
179
+ # @param channel_name [String] The channel to search for.
180
+ # @param server_name [String] The server to search for, or `nil` if only the channel should be searched for.
181
+ # @param type [String, nil] The type of channel to search for (`'text'` or `'voice'`), or `nil` if either type of
182
+ # channel should be searched for
183
+ # @return [Array<Channel>] The array of channels that were found. May be empty if none were found.
184
+ def find_channel(channel_name, server_name = nil, type: nil)
185
+ results = []
186
+
187
+ if /<#(?<id>\d+)>?/ =~ channel_name
188
+ # Check for channel mentions separately
189
+ return [channel(id)]
190
+ end
191
+
192
+ @servers.values.each do |server|
193
+ server.channels.each do |channel|
194
+ results << channel if channel.name == channel_name && (server_name || server.name) == server.name && (!type || (channel.type == type))
195
+ end
196
+ end
197
+
198
+ results
199
+ end
200
+
201
+ # Finds a user given its username.
202
+ # @param username [String] The username to look for.
203
+ # @return [Array<User>] The array of users that were found. May be empty if none were found.
204
+ def find_user(username)
205
+ @users.values.find_all { |e| e.username == username }
206
+ end
207
+ end
208
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'discordrb/bot'
2
4
  require 'discordrb/data'
3
5
  require 'discordrb/commands/parser'
@@ -19,22 +21,22 @@ module Discordrb::Commands
19
21
  include CommandContainer
20
22
 
21
23
  # Creates a new CommandBot and logs in to Discord.
22
- # @param email [String] The email to use to log in.
23
- # @param password [String] The password corresponding to the email.
24
- # @param prefix [String] The prefix that should trigger this bot's commands. Can be any string (including the empty
24
+ # @param attributes [Hash] The attributes to initialize the CommandBot with.
25
+ # @see {Discordrb::Bot#initialize} for other attributes that should be used to create the underlying regular bot.
26
+ # @option attributes [String] :prefix The prefix that should trigger this bot's commands. Can be any string (including the empty
25
27
  # string), but note that it will be literal - if the prefix is "hi" then the corresponding trigger string for
26
28
  # a command called "test" would be "hitest". Don't forget to put spaces in if you need them!
27
- # @param attributes [Hash] The attributes to initialize the CommandBot with.
28
- # @param debug [true, false] Whether or not debug mode should be used - debug mode logs tons of extra stuff to the
29
- # console that may be useful in development.
30
29
  # @option attributes [true, false] :advanced_functionality Whether to enable advanced functionality (very powerful
31
30
  # way to nest commands into chains, see https://github.com/meew0/discordrb/wiki/Commands#command-chain-syntax
32
31
  # for info. Default is true.
33
- # @option attributes [Symbol, Array<Symbol>] :help_command The name of the command that displays info for other
34
- # commands. Use an array if you want to have aliases. Default is "help".
32
+ # @option attributes [Symbol, Array<Symbol>, false] :help_command The name of the command that displays info for
33
+ # other commands. Use an array if you want to have aliases. Default is "help". If none should be created, use
34
+ # `false` as the value.
35
35
  # @option attributes [String] :command_doesnt_exist_message The message that should be displayed if a user attempts
36
36
  # to use a command that does not exist. If none is specified, no message will be displayed. In the message, you
37
37
  # can use the string '%command%' that will be replaced with the name of the command.
38
+ # @option attributes [true, false] :spaces_allowed Whether spaces are allowed to occur between the prefix and the
39
+ # command. Default is false.
38
40
  # @option attributes [String] :previous Character that should designate the result of the previous command in
39
41
  # a command chain (see :advanced_functionality). Default is '~'.
40
42
  # @option attributes [String] :chain_delimiter Character that should designate that a new command begins in the
@@ -49,20 +51,34 @@ module Discordrb::Commands
49
51
  # :advanced_functionality). Default is '"'.
50
52
  # @option attributes [String] :quote_end Character that should end a quoted string (see
51
53
  # :advanced_functionality). Default is '"'.
52
- def initialize(email, password, prefix, attributes = {}, debug = false)
53
- super(email, password, debug)
54
- @prefix = prefix
54
+ def initialize(attributes = {})
55
+ super(
56
+ email: attributes[:email],
57
+ password: attributes[:password],
58
+ log_mode: attributes[:log_mode],
59
+ token: attributes[:token],
60
+ application_id: attributes[:application_id],
61
+ type: attributes[:type],
62
+ name: attributes[:name],
63
+ fancy_log: attributes[:fancy_log],
64
+ suppress_ready: attributes[:suppress_ready],
65
+ parse_self: attributes[:parse_self])
66
+
67
+ @prefix = attributes[:prefix]
55
68
  @attributes = {
56
69
  # Whether advanced functionality such as command chains are enabled
57
- advanced_functionality: attributes[:advanced_functionality].nil? ? true : attributes[:advanced_functionality],
70
+ advanced_functionality: attributes[:advanced_functionality].nil? ? false : attributes[:advanced_functionality],
58
71
 
59
- # The name of the help command (that displays information to other commands). Nil if none should exist
60
- help_command: attributes[:help_command] || :help,
72
+ # The name of the help command (that displays information to other commands). False if none should exist
73
+ help_command: (attributes[:help_command].is_a? FalseClass) ? nil : (attributes[:help_command] || :help),
61
74
 
62
75
  # The message to display for when a command doesn't exist, %command% to get the command name in question and nil for no message
63
76
  # No default value here because it may not be desired behaviour
64
77
  command_doesnt_exist_message: attributes[:command_doesnt_exist_message],
65
78
 
79
+ # Spaces allowed between prefix and command
80
+ spaces_allowed: attributes[:spaces_allowed].nil? ? false : attributes[:spaces_allowed],
81
+
66
82
  # All of the following need to be one character
67
83
  # String to designate previous result in command chain
68
84
  previous: attributes[:previous] || '~',
@@ -99,7 +115,8 @@ module Discordrb::Commands
99
115
  desc = command.attributes[:description] || '*No description available*'
100
116
  usage = command.attributes[:usage]
101
117
  result = "**`#{command_name}`**: #{desc}"
102
- result << "\nUsage: `#{usage}`" if usage
118
+ result += "\nUsage: `#{usage}`" if usage
119
+ result
103
120
  else
104
121
  available_commands = @commands.values.reject { |c| !c.attributes[:help_available] }
105
122
  case available_commands.length
@@ -133,10 +150,10 @@ module Discordrb::Commands
133
150
  event.respond @attributes[:command_doesnt_exist_message].gsub('%command%', name.to_s) if @attributes[:command_doesnt_exist_message]
134
151
  return
135
152
  end
136
- if permission?(user(event.user.id), command.attributes[:permission_level], event.server)
153
+ if permission?(event.user, command.attributes[:permission_level], event.server)
137
154
  event.command = command
138
155
  result = command.call(event, arguments, chained)
139
- result.to_s
156
+ stringify(result)
140
157
  else
141
158
  event.respond "You don't have permission to execute command `#{name}`!"
142
159
  end
@@ -172,7 +189,7 @@ module Discordrb::Commands
172
189
  # @param server [Server] The server on which to check
173
190
  # @return [true, false] whether or not the user has the given permission
174
191
  def permission?(user, level, server)
175
- determined_level = server.nil? ? 0 : user.roles[server.id].each.reduce(0) do |memo, role|
192
+ determined_level = server.nil? ? 0 : user.roles.reduce(0) do |memo, role|
176
193
  [@permissions[:roles][role.id] || 0, memo].max
177
194
  end
178
195
  [@permissions[:users][user.id] || 0, determined_level].max >= level
@@ -183,11 +200,19 @@ module Discordrb::Commands
183
200
  # Internal handler for MESSAGE_CREATE that is overwritten to allow for command handling
184
201
  def create_message(data)
185
202
  message = Discordrb::Message.new(data, self)
203
+ return if message.from_bot? && !@should_parse_self
204
+
186
205
  event = CommandEvent.new(message, self)
187
206
 
188
207
  return unless message.content.start_with? @prefix
189
208
  chain = message.content[@prefix.length..-1]
190
209
 
210
+ # Don't allow spaces between the prefix and the command
211
+ if chain.start_with?(' ') && !@attributes[:spaces_allowed]
212
+ debug('Chain starts with a space')
213
+ return
214
+ end
215
+
191
216
  if chain.strip.empty?
192
217
  debug('Chain is empty')
193
218
  return
@@ -212,5 +237,12 @@ module Discordrb::Commands
212
237
  end
213
238
  end
214
239
  end
240
+
241
+ # Turns the object into a string, using to_s by default
242
+ def stringify(object)
243
+ return '' if object.is_a? Discordrb::Message
244
+
245
+ object.to_s
246
+ end
215
247
  end
216
248
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'discordrb/container'
2
4
  require 'discordrb/commands/rate_limiter'
3
5
 
@@ -34,8 +36,13 @@ module Discordrb::Commands
34
36
  def command(name, attributes = {}, &block)
35
37
  @commands ||= {}
36
38
  if name.is_a? Array
37
- new_command = Command.new(name[0], attributes, &block)
38
- name.each { |n| @commands[n] = new_command }
39
+ new_command = nil
40
+
41
+ name.each do |e|
42
+ new_command = Command.new(e, attributes, &block)
43
+ @commands[e] = new_command
44
+ end
45
+
39
46
  new_command
40
47
  else
41
48
  @commands[name] = Command.new(name, attributes, &block)
@@ -53,6 +60,8 @@ module Discordrb::Commands
53
60
  # @param container [Module] A module that `extend`s {CommandContainer} from which the commands will be added.
54
61
  def include_commands(container)
55
62
  handlers = container.instance_variable_get '@commands'
63
+ return unless handlers
64
+
56
65
  @commands ||= {}
57
66
  @commands.merge! handlers
58
67
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'discordrb/events/message'
2
4
 
3
5
  module Discordrb::Commands
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Discordrb::Commands
2
4
  # Command that can be called in a chain
3
5
  class Command
@@ -136,14 +138,14 @@ module Discordrb::Commands
136
138
  b_level += 1
137
139
  end
138
140
 
139
- result << char if b_level <= 0
141
+ result += char if b_level <= 0
140
142
 
141
143
  next unless char == @attributes[:sub_chain_end] && !quoted
142
144
  b_level -= 1
143
145
  next unless b_level == 0
144
146
  nested = @chain[b_start + 1..index - 1]
145
147
  subchain = CommandChain.new(nested, @bot, true)
146
- result << subchain.execute(event)
148
+ result += subchain.execute(event)
147
149
  end
148
150
 
149
151
  event.respond("Your subchains are mismatched! Make sure you don't have any extra #{@attributes[:sub_chain_start]}'s or #{@attributes[:sub_chain_end]}'s") unless b_level == 0
@@ -165,21 +167,21 @@ module Discordrb::Commands
165
167
  command = @attributes[:chain_delimiter] + command if first && @chain.start_with?(@attributes[:chain_delimiter])
166
168
  first = false
167
169
 
168
- command.strip!
170
+ command = command.strip
169
171
 
170
172
  # Replace the hacky delimiter that was used inside quotes with actual delimiters
171
- command.gsub! hacky_delim, @attributes[:chain_delimiter]
173
+ command = command.gsub hacky_delim, @attributes[:chain_delimiter]
172
174
 
173
175
  first_space = command.index ' '
174
176
  command_name = first_space ? command[0..first_space - 1] : command
175
177
  arguments = first_space ? command[first_space + 1..-1] : ''
176
178
 
177
179
  # Append a previous sign if none is present
178
- arguments << @attributes[:previous] unless arguments.include? @attributes[:previous]
179
- arguments.gsub! @attributes[:previous], prev
180
+ arguments += @attributes[:previous] unless arguments.include? @attributes[:previous]
181
+ arguments = arguments.gsub @attributes[:previous], prev
180
182
 
181
183
  # Replace hacky previous signs with actual ones
182
- arguments.gsub! hacky_prev, @attributes[:previous]
184
+ arguments = arguments.gsub hacky_prev, @attributes[:previous]
183
185
 
184
186
  arguments = arguments.split ' '
185
187
 
@@ -214,7 +216,7 @@ module Discordrb::Commands
214
216
  executed_chain = divide_chain(old_chain).last
215
217
 
216
218
  arg[1].to_i.times do
217
- new_result << CommandChain.new(executed_chain, @bot).execute(event)
219
+ new_result += CommandChain.new(executed_chain, @bot).execute(event)
218
220
  end
219
221
 
220
222
  result = new_result
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Discordrb::Commands
2
4
  # This class represents a bucket for rate limiting - it keeps track of how many requests have been made and when
3
5
  # exactly the user should be rate limited.