discordrb 3.4.3 → 3.6.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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  7. data/.github/workflows/ci.yml +78 -0
  8. data/.github/workflows/codeql.yml +65 -0
  9. data/.github/workflows/deploy.yml +54 -0
  10. data/.github/workflows/release.yml +45 -0
  11. data/.markdownlint.json +4 -0
  12. data/.rubocop.yml +58 -2
  13. data/CHANGELOG.md +485 -225
  14. data/LICENSE.txt +1 -1
  15. data/README.md +38 -26
  16. data/discordrb-webhooks.gemspec +4 -1
  17. data/discordrb.gemspec +18 -10
  18. data/lib/discordrb/api/application.rb +278 -0
  19. data/lib/discordrb/api/channel.rb +222 -18
  20. data/lib/discordrb/api/interaction.rb +63 -0
  21. data/lib/discordrb/api/invite.rb +2 -2
  22. data/lib/discordrb/api/server.rb +123 -66
  23. data/lib/discordrb/api/user.rb +20 -5
  24. data/lib/discordrb/api/webhook.rb +72 -0
  25. data/lib/discordrb/api.rb +35 -25
  26. data/lib/discordrb/bot.rb +437 -66
  27. data/lib/discordrb/cache.rb +41 -22
  28. data/lib/discordrb/commands/command_bot.rb +13 -21
  29. data/lib/discordrb/commands/container.rb +1 -1
  30. data/lib/discordrb/commands/parser.rb +7 -7
  31. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  32. data/lib/discordrb/container.rb +178 -3
  33. data/lib/discordrb/data/activity.rb +1 -1
  34. data/lib/discordrb/data/application.rb +1 -0
  35. data/lib/discordrb/data/attachment.rb +38 -3
  36. data/lib/discordrb/data/audit_logs.rb +3 -3
  37. data/lib/discordrb/data/avatar_decoration.rb +26 -0
  38. data/lib/discordrb/data/call.rb +22 -0
  39. data/lib/discordrb/data/channel.rb +299 -30
  40. data/lib/discordrb/data/collectibles.rb +45 -0
  41. data/lib/discordrb/data/component.rb +229 -0
  42. data/lib/discordrb/data/embed.rb +10 -3
  43. data/lib/discordrb/data/emoji.rb +20 -1
  44. data/lib/discordrb/data/integration.rb +45 -3
  45. data/lib/discordrb/data/interaction.rb +937 -0
  46. data/lib/discordrb/data/invite.rb +1 -1
  47. data/lib/discordrb/data/member.rb +236 -44
  48. data/lib/discordrb/data/message.rb +278 -51
  49. data/lib/discordrb/data/overwrite.rb +15 -7
  50. data/lib/discordrb/data/primary_server.rb +60 -0
  51. data/lib/discordrb/data/profile.rb +2 -7
  52. data/lib/discordrb/data/reaction.rb +2 -1
  53. data/lib/discordrb/data/recipient.rb +1 -1
  54. data/lib/discordrb/data/role.rb +204 -18
  55. data/lib/discordrb/data/server.rb +194 -118
  56. data/lib/discordrb/data/server_preview.rb +68 -0
  57. data/lib/discordrb/data/snapshot.rb +110 -0
  58. data/lib/discordrb/data/user.rb +132 -12
  59. data/lib/discordrb/data/voice_region.rb +1 -0
  60. data/lib/discordrb/data/webhook.rb +99 -9
  61. data/lib/discordrb/data.rb +9 -0
  62. data/lib/discordrb/errors.rb +47 -3
  63. data/lib/discordrb/events/await.rb +1 -1
  64. data/lib/discordrb/events/channels.rb +38 -1
  65. data/lib/discordrb/events/generic.rb +2 -0
  66. data/lib/discordrb/events/guilds.rb +6 -1
  67. data/lib/discordrb/events/interactions.rb +575 -0
  68. data/lib/discordrb/events/invites.rb +2 -0
  69. data/lib/discordrb/events/members.rb +19 -2
  70. data/lib/discordrb/events/message.rb +42 -8
  71. data/lib/discordrb/events/presence.rb +23 -14
  72. data/lib/discordrb/events/raw.rb +1 -0
  73. data/lib/discordrb/events/reactions.rb +2 -1
  74. data/lib/discordrb/events/roles.rb +2 -0
  75. data/lib/discordrb/events/threads.rb +100 -0
  76. data/lib/discordrb/events/typing.rb +1 -0
  77. data/lib/discordrb/events/voice_server_update.rb +1 -0
  78. data/lib/discordrb/events/voice_state_update.rb +1 -0
  79. data/lib/discordrb/events/webhooks.rb +1 -0
  80. data/lib/discordrb/gateway.rb +57 -28
  81. data/lib/discordrb/paginator.rb +3 -3
  82. data/lib/discordrb/permissions.rb +71 -35
  83. data/lib/discordrb/version.rb +1 -1
  84. data/lib/discordrb/voice/encoder.rb +2 -2
  85. data/lib/discordrb/voice/network.rb +18 -7
  86. data/lib/discordrb/voice/sodium.rb +3 -1
  87. data/lib/discordrb/voice/voice_bot.rb +3 -3
  88. data/lib/discordrb/webhooks.rb +2 -0
  89. data/lib/discordrb/websocket.rb +0 -10
  90. data/lib/discordrb.rb +54 -5
  91. metadata +87 -25
  92. data/.circleci/config.yml +0 -126
  93. data/.codeclimate.yml +0 -16
  94. data/.travis.yml +0 -32
  95. data/bin/travis_build_docs.sh +0 -17
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # A partial and immutable copy of a message that has been forwarded.
5
+ class Snapshot
6
+ # @return [Integer] the message type of the message snapshot.
7
+ attr_reader :type
8
+
9
+ # @return [String] the text content of the message snapshot.
10
+ attr_reader :content
11
+
12
+ # @return [Array<Embed>] the embeds attached to the message snapshot.
13
+ attr_reader :embeds
14
+
15
+ # @return [Array<Attachment>] the files attached to the message snapshot.
16
+ attr_reader :attachments
17
+
18
+ # @return [Time] the time at which the message snapshot was created.
19
+ attr_reader :created_at
20
+
21
+ # @return [Time, nil] the time at which the message snapshot was edited.
22
+ attr_reader :edited_at
23
+
24
+ # @return [Integer] the flags that have been set on the message snapshot.
25
+ attr_reader :flags
26
+
27
+ # @return [Array<User>] the users that were mentioned in the message snapshot.
28
+ attr_reader :mentions
29
+
30
+ # @return [Array<Component>] the interaction components associated with the message snapshot.
31
+ attr_reader :components
32
+
33
+ # @!visibility private
34
+ def initialize(data, bot)
35
+ @bot = bot
36
+ @type = data['type']
37
+ @flags = data['flags'] || 0
38
+ @content = data['content']
39
+ @mention_roles = data['mention_roles']&.map(&:resolve_id) || []
40
+ @embeds = data['embeds']&.map { |embed| Embed.new(embed, self) } || []
41
+ @attachments = data['attachments']&.map { |file| Attachment.new(file, self, bot) } || []
42
+ @created_at = data['timestamp'] ? Time.parse(data['timestamp']) : nil
43
+ @edited_at = data['edited_timestamp'] ? Time.parse(data['edited_timestamp']) : nil
44
+ @mentions = data['mentions']&.map { |mention| bot.ensure_user(mention) } || []
45
+ @components = data['components']&.map { |component| Components.from_data(component, bot) } || []
46
+ end
47
+
48
+ # Check whether the message snapshot has been edited.
49
+ # @return [true, false] whether the snapshot was edited or not.
50
+ def edited?
51
+ !@edited_at.nil?
52
+ end
53
+
54
+ # Check whether the message snapshot contains any custom emojis.
55
+ # @return [true, false] whether or not any emoji were used in the snapshot.
56
+ def emojis?
57
+ emojis.any?
58
+ end
59
+
60
+ # Get the custom emojis that were used in the message snapshot.
61
+ # @return [Array<Emoji>] the emojis used in the message snapshot.
62
+ def emojis
63
+ return [] if @content.nil? || @content.empty?
64
+
65
+ @emojis ||= @bot.parse_mentions(@content).select { |parsed| parsed.is_a?(Emoji) }
66
+ end
67
+
68
+ # Get the roles that were mentioned in the message snapshot.
69
+ # @return [Array<Role>] the roles that were mentioned in the message snapshot.
70
+ # @note this can only resolve roles in servers that the bot has access to via {Bot#servers}.
71
+ def role_mentions
72
+ return [] if @mention_roles.empty?
73
+
74
+ return @role_mentions if @role_mentions
75
+
76
+ roles = @bot.servers.values.flat_map(&:roles)
77
+
78
+ @role_mentions = @mention_roles.filter_map { |id| roles.find { |r| r.id == id } }
79
+ end
80
+
81
+ # Get the buttons that were used in the message snapshot.
82
+ # @return [Array<Components::Button>] the button components used in the message snapshot.
83
+ def buttons
84
+ buttons = @components.flat_map do |component|
85
+ case component
86
+ when Components::Button
87
+ component
88
+ when Components::ActionRow
89
+ component.buttons
90
+ end
91
+ end
92
+
93
+ buttons.compact
94
+ end
95
+
96
+ # @see Discordrb::Message::FLAGS
97
+ Message::FLAGS.each do |name, value|
98
+ define_method("#{name}?") do
99
+ @flags.anybits?(value)
100
+ end
101
+ end
102
+
103
+ # @see Discordrb::Message::TYPES
104
+ Message::TYPES.each do |name, value|
105
+ define_method("#{name}?") do
106
+ @type == value
107
+ end
108
+ end
109
+ end
110
+ end
@@ -3,10 +3,33 @@
3
3
  module Discordrb
4
4
  # Mixin for the attributes users should have
5
5
  module UserAttributes
6
+ # rubocop:disable Naming/VariableNumber
7
+ FLAGS = {
8
+ staff: 1 << 0,
9
+ partner: 1 << 1,
10
+ hypesquad: 1 << 2,
11
+ bug_hunter_level_1: 1 << 3,
12
+ hypesquad_online_house_1: 1 << 6,
13
+ hypesquad_online_house_2: 1 << 7,
14
+ hypesquad_online_house_3: 1 << 8,
15
+ premium_early_supporter: 1 << 9,
16
+ team_pseudo_user: 1 << 10,
17
+ bug_hunter_level_2: 1 << 14,
18
+ verified_bot: 1 << 16,
19
+ verified_developer: 1 << 17,
20
+ certified_moderator: 1 << 18,
21
+ bot_http_interactions: 1 << 19,
22
+ active_developer: 1 << 22
23
+ }.freeze
24
+ # rubocop:enable Naming/VariableNumber
25
+
6
26
  # @return [String] this user's username
7
27
  attr_reader :username
8
28
  alias_method :name, :username
9
29
 
30
+ # @return [String, nil] this user's global name
31
+ attr_reader :global_name
32
+
10
33
  # @return [String] this user's discriminator which is used internally to identify users with identical usernames.
11
34
  attr_reader :discriminator
12
35
  alias_method :discrim, :discriminator
@@ -17,10 +40,35 @@ module Discordrb
17
40
  attr_reader :bot_account
18
41
  alias_method :bot_account?, :bot_account
19
42
 
43
+ # @return [true, false] whether this is fake user for a webhook message
44
+ attr_reader :webhook_account
45
+ alias_method :webhook_account?, :webhook_account
46
+ alias_method :webhook?, :webhook_account
47
+
20
48
  # @return [String] the ID of this user's current avatar, can be used to generate an avatar URL.
21
49
  # @see #avatar_url
22
50
  attr_accessor :avatar_id
23
51
 
52
+ # @return [true, false] whether the user is an offical Discord System user (part of the urgent message system).
53
+ attr_reader :system_account
54
+ alias_method :system_account?, :system_account
55
+
56
+ # @return [AvatarDecoration, nil] the current user's avatar decoration, or nil if the user doesn't have one.
57
+ attr_reader :avatar_decoration
58
+
59
+ # @return [Collectibles] the collectibles that this user has collected.
60
+ attr_reader :collectibles
61
+
62
+ # @return [PrimaryServer, nil] the server tag the user has adopted, or nil if the user doesn't have one displayed.
63
+ attr_reader :primary_server
64
+ alias_method :server_tag, :primary_server
65
+
66
+ # Utility function to get Discord's display name of a user not in server
67
+ # @return [String] the name the user displays as (global_name if they have one, username otherwise)
68
+ def display_name
69
+ global_name || username
70
+ end
71
+
24
72
  # Utility function to mention users in messages
25
73
  # @return [String] the mention code in the form of <@id>
26
74
  def mention
@@ -29,18 +77,45 @@ module Discordrb
29
77
 
30
78
  # Utility function to get Discord's distinct representation of a user, i.e. username + discriminator
31
79
  # @return [String] distinct representation of user
80
+ # TODO: Maybe change this method again after discriminator removal ?
32
81
  def distinct
33
- "#{@username}##{@discriminator}"
82
+ if @discriminator && @discriminator != '0'
83
+ "#{@username}##{@discriminator}"
84
+ else
85
+ @username.to_s
86
+ end
34
87
  end
35
88
 
36
89
  # Utility function to get a user's avatar URL.
37
90
  # @param format [String, nil] If `nil`, the URL will default to `webp` for static avatars, and will detect if the user has a `gif` avatar. You can otherwise specify one of `webp`, `jpg`, `png`, or `gif` to override this. Will always be PNG for default avatars.
38
91
  # @return [String] the URL to the avatar image.
92
+ # TODO: Maybe change this method again after discriminator removal ?
39
93
  def avatar_url(format = nil)
40
- return API::User.default_avatar(@discriminator) unless @avatar_id
94
+ unless @avatar_id
95
+ return API::User.default_avatar(@discriminator, legacy: true) if @discriminator && @discriminator != '0'
96
+
97
+ return API::User.default_avatar(@id)
98
+ end
41
99
 
42
100
  API::User.avatar_url(@id, @avatar_id, format)
43
101
  end
102
+
103
+ # @return [Integer] the public flags on a user's account
104
+ attr_reader :public_flags
105
+
106
+ FLAGS.each do |name, value|
107
+ define_method("#{name}?") do
108
+ @public_flags.anybits?(value)
109
+ end
110
+ end
111
+
112
+ # Utility function to get a user's banner URL.
113
+ # @param format [String, nil] If `nil`, the URL will default to `png` for static banners and will detect if the user has a `gif` banner.
114
+ # You can otherwise specify one of `webp`, `jpg`, `png`, or `gif` to override this.
115
+ # @return [String, nil] the URL to the banner image or nil if the user doesn't have one.
116
+ def banner_url(format = nil)
117
+ API::User.banner_url(@id, banner_id, format) if banner_id
118
+ end
44
119
  end
45
120
 
46
121
  # User on Discord, including internal data like discriminators
@@ -63,17 +138,23 @@ module Discordrb
63
138
  @bot = bot
64
139
 
65
140
  @username = data['username']
141
+ @global_name = data['global_name']
66
142
  @id = data['id'].to_i
67
143
  @discriminator = data['discriminator']
68
144
  @avatar_id = data['avatar']
69
- @roles = {}
70
145
  @activities = Discordrb::ActivitySet.new
71
-
72
- @bot_account = false
73
- @bot_account = true if data['bot']
146
+ @public_flags = data['public_flags'] || 0
147
+ @bot_account = data['bot'] || false
148
+ @webhook_account = data['_webhook'] || false
74
149
 
75
150
  @status = :offline
76
151
  @client_status = process_client_status(data['client_status'])
152
+ @banner_id = data['banner']
153
+ @system_account = data['system'] || false
154
+ @avatar_decoration = process_avatar_decoration(data['avatar_decoration_data'])
155
+ @collectibles = Collectibles.new(data['collectibles'] || {}, bot)
156
+
157
+ @primary_server = process_primary_server(data['primary_guild'] || {})
77
158
  end
78
159
 
79
160
  # Get a user's PM channel or send them a PM
@@ -109,13 +190,47 @@ module Discordrb
109
190
  pm.send_file(file, caption: caption, filename: filename, spoiler: spoiler)
110
191
  end
111
192
 
112
- # Set the user's name
193
+ # @return [String, nil] the ID of this user's current banner, can be used to generate a banner URL.
194
+ # @see #banner_url
195
+ def banner_id
196
+ @banner_id ||= JSON.parse(API::User.resolve(@bot.token, @id))['banner']
197
+ end
198
+
199
+ # Set the user's username
113
200
  # @note for internal use only
114
201
  # @!visibility private
115
202
  def update_username(username)
116
203
  @username = username
117
204
  end
118
205
 
206
+ # Set the user's global_name
207
+ # @note For internal use only.
208
+ # @!visibility private
209
+ def update_global_name(global_name)
210
+ @global_name = global_name
211
+ end
212
+
213
+ # Set the user's avatar_decoration
214
+ # @note For internal use only.
215
+ # @!visibility private
216
+ def update_avatar_decoration(decoration)
217
+ @avatar_decoration = process_avatar_decoration(decoration)
218
+ end
219
+
220
+ # Set the user's collectibles
221
+ # @note For internal use only.
222
+ # @!visibility private
223
+ def update_collectibles(collectibles)
224
+ @collectibles = Collectibles.new(collectibles || {}, @bot)
225
+ end
226
+
227
+ # Set the user's primary server
228
+ # @note For internal use only.
229
+ # @!visibility private
230
+ def update_primary_server(server)
231
+ @primary_server = process_primary_server(server || {})
232
+ end
233
+
119
234
  # Set the user's presence data
120
235
  # @note for internal use only
121
236
  # @!visibility private
@@ -154,14 +269,19 @@ module Discordrb
154
269
  @bot.profile.id == @id
155
270
  end
156
271
 
157
- # @return [true, false] whether this user is a fake user for a webhook message
158
- def webhook?
159
- @discriminator == Message::ZERO_DISCRIM
272
+ # @!visibility private
273
+ def process_client_status(client_status)
274
+ (client_status || {}).to_h { |k, v| [k.to_sym, v.to_sym] }
275
+ end
276
+
277
+ # @!visibility private
278
+ def process_avatar_decoration(decoration)
279
+ decoration ? AvatarDecoration.new(decoration, @bot) : nil
160
280
  end
161
281
 
162
282
  # @!visibility private
163
- def process_client_status(client_status)
164
- (client_status || {}).map { |k, v| [k.to_sym, v.to_sym] }.to_h
283
+ def process_primary_server(server)
284
+ PrimaryServer.new(server, @bot) if server['identity_enabled']
165
285
  end
166
286
 
167
287
  # @!method offline?
@@ -28,6 +28,7 @@ module Discordrb
28
28
  # @return [true, false] whether this is a custom voice region (used for events/etc)
29
29
  attr_reader :custom
30
30
 
31
+ # @!visibility private
31
32
  def initialize(data)
32
33
  @id = data['id']
33
34
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'discordrb/webhooks/builder'
4
+ require 'discordrb/webhooks/view'
5
+
3
6
  module Discordrb
4
7
  # A webhook on a server channel
5
8
  class Webhook
@@ -28,6 +31,7 @@ module Discordrb
28
31
  # @return [Member, User, nil] the user object of the owner or nil if the webhook was requested using the token.
29
32
  attr_reader :owner
30
33
 
34
+ # @!visibility private
31
35
  def initialize(data, bot)
32
36
  @bot = bot
33
37
 
@@ -81,13 +85,13 @@ module Discordrb
81
85
  def update(data)
82
86
  # Only pass a value for avatar if the key is defined as sending nil will delete the
83
87
  data[:avatar] = avatarise(data[:avatar]) if data.key?(:avatar)
84
- data[:channel_id] = data[:channel].resolve_id
88
+ data[:channel_id] = data[:channel]&.resolve_id
85
89
  data.delete(:channel)
86
- update_webhook(data)
90
+ update_webhook(**data)
87
91
  end
88
92
 
89
93
  # Deletes the webhook.
90
- # @param reason [String] The reason the invite is being deleted.
94
+ # @param reason [String] The reason the webhook is being deleted.
91
95
  def delete(reason = nil)
92
96
  if token?
93
97
  API::Webhook.token_delete_webhook(@token, @id, reason)
@@ -96,10 +100,100 @@ module Discordrb
96
100
  end
97
101
  end
98
102
 
103
+ # Execute a webhook.
104
+ # @param content [String] The content of the message. May be 2000 characters long at most.
105
+ # @param username [String] The username the webhook will display as. If this is not set, the default username set in the webhook's settings.
106
+ # @param avatar_url [String] The URL of an image file to be used as an avatar. If this is not set, the default avatar from the webhook's
107
+ # @param tts [true, false] Whether this message should use TTS or not. By default, it doesn't.
108
+ # @param file [File] File to be sent together with the message. Mutually exclusive with embeds; a webhook message can contain
109
+ # either a file to be sent or embeds.
110
+ # @param embeds [Array<Webhooks::Embed, Hash>] Embeds to attach to this message.
111
+ # @param allowed_mentions [AllowedMentions, Hash] Mentions that are allowed to ping in the `content`.
112
+ # @param wait [true, false] Whether Discord should wait for the message to be successfully received by clients, or
113
+ # whether it should return immediately after sending the message. If `true` a {Message} object will be returned.
114
+ # @yield [builder] Gives the builder to the block to add additional steps, or to do the entire building process.
115
+ # @yieldparam builder [Builder] The builder given as a parameter which is used as the initial step to start from.
116
+ # @example Execute the webhook with kwargs
117
+ # client.execute(
118
+ # content: 'Testing',
119
+ # username: 'discordrb',
120
+ # embeds: [
121
+ # { timestamp: Time.now.iso8601, title: 'testing', image: { url: 'https://i.imgur.com/PcMltU7.jpg' } }
122
+ # ])
123
+ # @example Execute the webhook with an already existing builder
124
+ # builder = Discordrb::Webhooks::Builder.new # ...
125
+ # client.execute(builder)
126
+ # @example Execute the webhook by building a new message
127
+ # client.execute do |builder|
128
+ # builder.content = 'Testing'
129
+ # builder.username = 'discordrb'
130
+ # builder.add_embed do |embed|
131
+ # embed.timestamp = Time.now
132
+ # embed.title = 'Testing'
133
+ # embed.image = Discordrb::Webhooks::EmbedImage.new(url: 'https://i.imgur.com/PcMltU7.jpg')
134
+ # end
135
+ # end
136
+ # @return [Message, nil] If `wait` is `true`, a {Message} will be returned. Otherwise this method will return `nil`.
137
+ # @note This is only available to webhooks with publically exposed tokens. This excludes channel follow webhooks and webhooks retrieved
138
+ # via the audit log.
139
+ def execute(content: nil, username: nil, avatar_url: nil, tts: nil, file: nil, embeds: nil, allowed_mentions: nil, wait: true, builder: nil, components: nil)
140
+ raise Discordrb::Errors::UnauthorizedWebhook unless @token
141
+
142
+ params = { content: content, username: username, avatar_url: avatar_url, tts: tts, file: file, embeds: embeds, allowed_mentions: allowed_mentions }
143
+
144
+ builder ||= Webhooks::Builder.new
145
+ view = Webhooks::View.new
146
+
147
+ yield(builder, view) if block_given?
148
+
149
+ data = builder.to_json_hash.merge(params.compact)
150
+ components ||= view
151
+
152
+ resp = API::Webhook.token_execute_webhook(@token, @id, wait, data[:content], data[:username], data[:avatar_url], data[:tts], data[:file], data[:embeds], data[:allowed_mentions], nil, components.to_a)
153
+
154
+ Message.new(JSON.parse(resp), @bot) if wait
155
+ end
156
+
157
+ # Delete a message created by this webhook.
158
+ # @param message [Message, String, Integer] The ID of the message to delete.
159
+ def delete_message(message)
160
+ raise Discordrb::Errors::UnauthorizedWebhook unless @token
161
+
162
+ API::Webhook.token_delete_message(@token, @id, message.resolve_id)
163
+ end
164
+
165
+ # Edit a message created by this webhook.
166
+ # @param message [Message, String, Integer] The ID of the message to edit.
167
+ # @param content [String] The content of the message. May be 2000 characters long at most.
168
+ # @param embeds [Array<Webhooks::Embed, Hash>] Embeds to be attached to the message.
169
+ # @param allowed_mentions [AllowedMentions, Hash] Mentions that are allowed to ping in the `content`.
170
+ # @param builder [Builder, nil] The builder to start out with, or nil if one should be created anew.
171
+ # @yield [builder] Gives the builder to the block to add additional steps, or to do the entire building process.
172
+ # @yieldparam builder [Webhooks::Builder] The builder given as a parameter which is used as the initial step to start from.
173
+ # @return [Message] The updated message.
174
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
175
+ # @note When editing `allowed_mentions`, it will update visually in the client but not alert the user with a notification.
176
+ def edit_message(message, content: nil, embeds: nil, allowed_mentions: nil, builder: nil, components: nil)
177
+ raise Discordrb::Errors::UnauthorizedWebhook unless @token
178
+
179
+ params = { content: content, embeds: embeds, allowed_mentions: allowed_mentions }.compact
180
+
181
+ builder ||= Webhooks::Builder.new
182
+ view ||= Webhooks::View.new
183
+
184
+ yield(builder, view) if block_given?
185
+
186
+ data = builder.to_json_hash.merge(params.compact)
187
+ components ||= view
188
+
189
+ resp = API::Webhook.token_edit_message(@token, @id, message.resolve_id, data[:content], data[:embeds], data[:allowed_mentions], components.to_a)
190
+ Message.new(JSON.parse(resp), @bot)
191
+ end
192
+
99
193
  # Utility function to get a webhook's avatar URL.
100
194
  # @return [String] the URL to the avatar image
101
195
  def avatar_url
102
- return API::User.default_avatar unless @avatar
196
+ return API::User.default_avatar(@id) unless @avatar
103
197
 
104
198
  API::User.avatar_url(@id, @avatar)
105
199
  end
@@ -118,11 +212,7 @@ module Discordrb
118
212
  private
119
213
 
120
214
  def avatarise(avatar)
121
- if avatar.respond_to? :read
122
- "data:image/jpg;base64,#{Base64.strict_encode64(avatar.read)}"
123
- else
124
- avatar
125
- end
215
+ avatar.respond_to?(:read) ? Discordrb.encode64(avatar) : avatar
126
216
  end
127
217
 
128
218
  def update_internal(data)
@@ -12,6 +12,7 @@ require 'discordrb/api/invite'
12
12
  require 'discordrb/api/user'
13
13
  require 'discordrb/api/webhook'
14
14
  require 'discordrb/webhooks/embeds'
15
+ require 'discordrb/webhooks/view'
15
16
  require 'discordrb/paginator'
16
17
  require 'time'
17
18
  require 'base64'
@@ -37,3 +38,11 @@ require 'discordrb/data/integration'
37
38
  require 'discordrb/data/server'
38
39
  require 'discordrb/data/webhook'
39
40
  require 'discordrb/data/audit_logs'
41
+ require 'discordrb/data/interaction'
42
+ require 'discordrb/data/component'
43
+ require 'discordrb/data/avatar_decoration'
44
+ require 'discordrb/data/collectibles'
45
+ require 'discordrb/data/primary_server'
46
+ require 'discordrb/data/server_preview'
47
+ require 'discordrb/data/call'
48
+ require 'discordrb/data/snapshot'
@@ -20,6 +20,9 @@ module Discordrb
20
20
  # Raised when the bot gets a HTTP 502 error, which is usually caused by Cloudflare.
21
21
  class CloudflareError < RuntimeError; end
22
22
 
23
+ # Raised when using a webhook method without an associated token.
24
+ class UnauthorizedWebhook < RuntimeError; end
25
+
23
26
  # Generic class for errors denoted by API error codes
24
27
  class CodeError < RuntimeError
25
28
  class << self
@@ -29,8 +32,11 @@ module Discordrb
29
32
 
30
33
  # Create a new error with a particular message (the code should be defined by the class instance variable)
31
34
  # @param message [String] the message to use
32
- def initialize(message)
35
+ # @param errors [Hash] API errors
36
+ def initialize(message, errors = nil)
33
37
  @message = message
38
+
39
+ @errors = errors ? flatten_errors(errors) : []
34
40
  end
35
41
 
36
42
  # @return [Integer] The error code represented by this error.
@@ -38,15 +44,50 @@ module Discordrb
38
44
  self.class.code
39
45
  end
40
46
 
47
+ # @return [String] A message including the message and flattened errors.
48
+ def full_message(*)
49
+ error_list = @errors.collect { |err| "\t- #{err}" }
50
+
51
+ "#{@message}\n#{error_list.join("\n")}"
52
+ end
53
+
41
54
  # @return [String] This error's represented message
42
55
  attr_reader :message
56
+
57
+ # @return [Hash] More precise errors
58
+ attr_reader :errors
59
+
60
+ private
61
+
62
+ # @!visibility hidden
63
+ # Flattens errors into a more easily read format.
64
+ # @example Flattening errors of a bad field
65
+ # flatten_errors(data['errors'])
66
+ # # => ["embed.fields[0].name: This field is required", "embed.fields[0].value: This field is required"]
67
+ def flatten_errors(err, prev_key = nil)
68
+ err.collect do |key, sub_err|
69
+ if prev_key
70
+ key = /\A\d+\Z/.match?(key) ? "#{prev_key}[#{key}]" : "#{prev_key}.#{key}"
71
+ end
72
+
73
+ if (errs = sub_err['_errors'])
74
+ "#{key}: #{errs.map { |e| e['message'] }.join(' ')}"
75
+ elsif sub_err['message'] || sub_err['code']
76
+ "#{"#{sub_err['code']}: " if sub_err['code']}#{err_msg}"
77
+ elsif sub_err.is_a? String
78
+ sub_err
79
+ else
80
+ flatten_errors(sub_err, key)
81
+ end
82
+ end.flatten
83
+ end
43
84
  end
44
85
 
45
86
  # Create a new code error class
46
87
  # rubocop:disable Naming/MethodName
47
88
  def self.Code(code)
48
89
  classy = Class.new(CodeError)
49
- classy.instance_variable_set('@code', code)
90
+ classy.instance_variable_set(:@code, code)
50
91
 
51
92
  @code_classes ||= {}
52
93
  @code_classes[code] = classy
@@ -58,7 +99,7 @@ module Discordrb
58
99
  # @param code [Integer] The code to check
59
100
  # @return [Class] the error class for the given code
60
101
  def self.error_class_for(code)
61
- @code_classes[code]
102
+ @code_classes[code] || Code(code)
62
103
  end
63
104
 
64
105
  # Used when Discord doesn't provide a more specific code
@@ -133,6 +174,9 @@ module Discordrb
133
174
  # Unauthorized
134
175
  Unauthorized = Unauthorised = Code(40_001)
135
176
 
177
+ # Unable to bulk ban any users
178
+ UnableToBulkBanUsers = Code(500_000)
179
+
136
180
  # Missing Access
137
181
  MissingAccess = Code(50_001)
138
182
 
@@ -25,7 +25,7 @@ module Discordrb::Events
25
25
  # @see Await#attributes
26
26
  delegate :key, :type, :attributes, to: :await
27
27
 
28
- # For internal use only
28
+ # @!visibility private
29
29
  def initialize(await, event, bot)
30
30
  @await = await
31
31
  @event = event
@@ -29,9 +29,10 @@ module Discordrb::Events
29
29
  # @see Channel#server
30
30
  delegate :name, :server, :type, :owner_id, :recipients, :topic, :user_limit, :position, :permission_overwrites, to: :channel
31
31
 
32
+ # @!visibility private
32
33
  def initialize(data, bot)
33
34
  @bot = bot
34
- @channel = bot.channel(data['id'].to_i)
35
+ @channel = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
35
36
  end
36
37
  end
37
38
 
@@ -83,6 +84,7 @@ module Discordrb::Events
83
84
  # @return [Integer, nil] the channel's owner ID if this is a group channel
84
85
  attr_reader :owner_id
85
86
 
87
+ # @!visibility private
86
88
  def initialize(data, bot)
87
89
  @bot = bot
88
90
 
@@ -134,6 +136,7 @@ module Discordrb::Events
134
136
 
135
137
  delegate :id, to: :recipient
136
138
 
139
+ # @!visibility private
137
140
  def initialize(data, bot)
138
141
  @bot = bot
139
142
 
@@ -168,6 +171,40 @@ module Discordrb::Events
168
171
  end
169
172
  end
170
173
 
174
+ # Raised when a message is pinned or unpinned.
175
+ class ChannelPinsUpdateEvent < Event
176
+ # @return [Time, nil] Time at which the most recent pinned message was pinned.
177
+ attr_reader :last_pin_timestamp
178
+
179
+ # @return [Channel] The channel this event originates from.
180
+ attr_reader :channel
181
+
182
+ # @return [Server, nil] The server this event originates from.
183
+ attr_reader :server
184
+
185
+ # @!visibility private
186
+ def initialize(data, bot)
187
+ @bot = bot
188
+
189
+ @server = bot.server(data['guild_id']) if data['guild_id']
190
+ @channel = bot.channel(data['channel_id'])
191
+ @last_pin_timestamp = Time.iso8601(data['last_pin_timestamp']) if data['last_pin_timestamp']
192
+ end
193
+ end
194
+
195
+ # Event handler for ChannelPinsUpdateEvent.
196
+ class ChannelPinsUpdateEventHandler < EventHandler
197
+ def matches?(event)
198
+ # Check for the proper event type.
199
+ return false unless event.is_a? ChannelPinsUpdateEvent
200
+
201
+ [
202
+ matches_all(@attributes[:server], event.server) { |a, e| a.resolve_id == e&.id },
203
+ matches_all(@attributes[:channel], event.channel) { |a, e| a.resolve_id == e.id }
204
+ ].reduce(true, &:&)
205
+ end
206
+ end
207
+
171
208
  # Raised when a user is added to a private channel
172
209
  class ChannelRecipientAddEvent < ChannelRecipientEvent; end
173
210