discordrb 3.4.3 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +44 -18
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -1
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  5. data/.github/workflows/codeql.yml +65 -0
  6. data/.markdownlint.json +4 -0
  7. data/.rubocop.yml +8 -2
  8. data/CHANGELOG.md +390 -225
  9. data/LICENSE.txt +1 -1
  10. data/README.md +37 -25
  11. data/discordrb-webhooks.gemspec +4 -1
  12. data/discordrb.gemspec +9 -6
  13. data/lib/discordrb/api/application.rb +202 -0
  14. data/lib/discordrb/api/channel.rb +177 -11
  15. data/lib/discordrb/api/interaction.rb +54 -0
  16. data/lib/discordrb/api/invite.rb +2 -2
  17. data/lib/discordrb/api/server.rb +40 -19
  18. data/lib/discordrb/api/user.rb +8 -3
  19. data/lib/discordrb/api/webhook.rb +57 -0
  20. data/lib/discordrb/api.rb +19 -5
  21. data/lib/discordrb/bot.rb +317 -32
  22. data/lib/discordrb/cache.rb +27 -22
  23. data/lib/discordrb/commands/command_bot.rb +6 -4
  24. data/lib/discordrb/commands/container.rb +1 -1
  25. data/lib/discordrb/commands/parser.rb +2 -2
  26. data/lib/discordrb/commands/rate_limiter.rb +1 -1
  27. data/lib/discordrb/container.rb +132 -3
  28. data/lib/discordrb/data/attachment.rb +15 -0
  29. data/lib/discordrb/data/audit_logs.rb +3 -3
  30. data/lib/discordrb/data/channel.rb +167 -23
  31. data/lib/discordrb/data/component.rb +229 -0
  32. data/lib/discordrb/data/integration.rb +42 -3
  33. data/lib/discordrb/data/interaction.rb +800 -0
  34. data/lib/discordrb/data/invite.rb +1 -1
  35. data/lib/discordrb/data/member.rb +108 -33
  36. data/lib/discordrb/data/message.rb +99 -19
  37. data/lib/discordrb/data/overwrite.rb +13 -7
  38. data/lib/discordrb/data/role.rb +58 -1
  39. data/lib/discordrb/data/server.rb +82 -80
  40. data/lib/discordrb/data/user.rb +69 -9
  41. data/lib/discordrb/data/webhook.rb +97 -4
  42. data/lib/discordrb/data.rb +3 -0
  43. data/lib/discordrb/errors.rb +44 -3
  44. data/lib/discordrb/events/channels.rb +1 -1
  45. data/lib/discordrb/events/interactions.rb +482 -0
  46. data/lib/discordrb/events/message.rb +9 -6
  47. data/lib/discordrb/events/presence.rb +21 -14
  48. data/lib/discordrb/events/reactions.rb +0 -1
  49. data/lib/discordrb/events/threads.rb +96 -0
  50. data/lib/discordrb/gateway.rb +30 -17
  51. data/lib/discordrb/permissions.rb +59 -34
  52. data/lib/discordrb/version.rb +1 -1
  53. data/lib/discordrb/voice/encoder.rb +2 -2
  54. data/lib/discordrb/voice/network.rb +18 -7
  55. data/lib/discordrb/voice/sodium.rb +3 -1
  56. data/lib/discordrb/voice/voice_bot.rb +3 -3
  57. data/lib/discordrb/webhooks.rb +2 -0
  58. data/lib/discordrb.rb +37 -4
  59. metadata +48 -14
  60. data/.codeclimate.yml +0 -16
  61. data/.travis.yml +0 -32
  62. data/bin/travis_build_docs.sh +0 -17
@@ -66,28 +66,15 @@ module Discordrb
66
66
  @bot = bot
67
67
  @owner_id = data['owner_id'].to_i
68
68
  @id = data['id'].to_i
69
-
70
- process_channels(data['channels'])
71
- update_data(data)
72
-
73
- @large = data['large']
74
- @member_count = data['member_count']
75
- @splash_id = nil
76
- @banner_id = nil
77
- @features = data['features'].map { |element| element.downcase.to_sym }
78
69
  @members = {}
79
70
  @voice_states = {}
80
71
  @emoji = {}
81
72
 
82
- process_roles(data['roles'])
83
- process_emoji(data['emojis'])
84
- process_members(data['members'])
85
- process_presences(data['presences'])
86
- process_voice_states(data['voice_states'])
73
+ process_channels(data['channels'])
74
+ update_data(data)
87
75
 
88
76
  # Whether this server's members have been chunked (resolved using op 8 and GUILD_MEMBERS_CHUNK) yet
89
77
  @chunked = false
90
- @processed_chunk_members = 0
91
78
 
92
79
  @booster_count = data['premium_subscription_count'] || 0
93
80
  @boost_level = data['premium_tier']
@@ -143,10 +130,15 @@ module Discordrb
143
130
  end
144
131
 
145
132
  # @return [Array<Member>] an array of all the members on this server.
133
+ # @raise [RuntimeError] if the bot was not started with the :server_member intent
146
134
  def members
147
135
  return @members.values if @chunked
148
136
 
149
137
  @bot.debug("Members for server #{@id} not chunked yet - initiating")
138
+
139
+ # If the SERVER_MEMBERS intent flag isn't set, the gateway won't respond when we ask for members.
140
+ raise 'The :server_members intent is required to get server members' if (@bot.gateway.intents & INTENTS[:server_members]).zero?
141
+
150
142
  @bot.request_chunks(@id)
151
143
  sleep 0.05 until @chunked
152
144
  @members.values
@@ -189,78 +181,73 @@ module Discordrb
189
181
  AuditLogs.new(self, @bot, JSON.parse(API::Server.audit_logs(@bot.token, @id, limit, user, action, before)))
190
182
  end
191
183
 
192
- # Cache @embed
184
+ # Cache @widget
193
185
  # @note For internal use only
194
186
  # @!visibility private
195
- def cache_embed_data
196
- data = JSON.parse(API::Server.embed(@bot.token, @id))
197
- @embed_enabled = data['enabled']
198
- @embed_channel_id = data['channel_id']
187
+ def cache_widget_data
188
+ data = JSON.parse(API::Server.widget(@bot.token, @id))
189
+ @widget_enabled = data['enabled']
190
+ @widget_channel_id = data['channel_id']
199
191
  end
200
192
 
201
193
  # @return [true, false] whether or not the server has widget enabled
202
- def embed_enabled?
203
- cache_embed_data if @embed_enabled.nil?
204
- @embed_enabled
194
+ def widget_enabled?
195
+ cache_widget_data if @widget_enabled.nil?
196
+ @widget_enabled
205
197
  end
206
- alias_method :widget_enabled, :embed_enabled?
207
- alias_method :widget?, :embed_enabled?
208
- alias_method :embed?, :embed_enabled?
198
+ alias_method :widget?, :widget_enabled?
199
+ alias_method :embed_enabled, :widget_enabled?
200
+ alias_method :embed?, :widget_enabled?
209
201
 
210
- # @return [Channel, nil] the channel the server embed will make an invite for.
211
- def embed_channel
212
- cache_embed_data if @embed_enabled.nil?
213
- @bot.channel(@embed_channel_id) if @embed_channel_id
202
+ # @return [Channel, nil] the channel the server widget will make an invite for.
203
+ def widget_channel
204
+ cache_widget_data if @widget_enabled.nil?
205
+ @bot.channel(@widget_channel_id) if @widget_channel_id
214
206
  end
215
- alias_method :widget_channel, :embed_channel
207
+ alias_method :embed_channel, :widget_channel
216
208
 
217
- # Sets whether this server's embed (widget) is enabled
209
+ # Sets whether this server's widget is enabled
218
210
  # @param value [true, false]
219
- def embed_enabled=(value)
220
- modify_embed(value, embed_channel)
211
+ def widget_enabled=(value)
212
+ modify_widget(value, widget_channel)
221
213
  end
214
+ alias_method :embed_enabled=, :widget_enabled=
222
215
 
223
- alias_method :widget_enabled=, :embed_enabled=
224
-
225
- # Sets whether this server's embed (widget) is enabled
216
+ # Sets whether this server's widget is enabled
226
217
  # @param value [true, false]
227
218
  # @param reason [String, nil] the reason to be shown in the audit log for this action
228
- def set_embed_enabled(value, reason = nil)
229
- modify_embed(value, embed_channel, reason)
219
+ def set_widget_enabled(value, reason = nil)
220
+ modify_widget(value, widget_channel, reason)
230
221
  end
222
+ alias_method :set_embed_enabled, :set_widget_enabled
231
223
 
232
- alias_method :set_widget_enabled, :set_embed_enabled
233
-
234
- # Changes the channel on the server's embed (widget)
235
- # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the embed
236
- def embed_channel=(channel)
237
- modify_embed(embed?, channel)
224
+ # Changes the channel on the server's widget
225
+ # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the widget
226
+ def widget_channel=(channel)
227
+ modify_widget(widget?, channel)
238
228
  end
229
+ alias_method :embed_channel=, :widget_channel=
239
230
 
240
- alias_method :widget_channel=, :embed_channel=
241
-
242
- # Changes the channel on the server's embed (widget)
243
- # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the embed
231
+ # Changes the channel on the server's widget
232
+ # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the widget
244
233
  # @param reason [String, nil] the reason to be shown in the audit log for this action
245
- def set_embed_channel(channel, reason = nil)
246
- modify_embed(embed?, channel, reason)
234
+ def set_widget_channel(channel, reason = nil)
235
+ modify_widget(widget?, channel, reason)
247
236
  end
237
+ alias_method :set_embed_channel, :set_widget_channel
248
238
 
249
- alias_method :set_widget_channel, :set_embed_channel
250
-
251
- # Changes the channel on the server's embed (widget), and sets whether it is enabled.
252
- # @param enabled [true, false] whether the embed (widget) is enabled
253
- # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the embed
239
+ # Changes the channel on the server's widget, and sets whether it is enabled.
240
+ # @param enabled [true, false] whether the widget is enabled
241
+ # @param channel [Channel, String, Integer] the channel, or its ID, to be referenced by the widget
254
242
  # @param reason [String, nil] the reason to be shown in the audit log for this action
255
- def modify_embed(enabled, channel, reason = nil)
256
- cache_embed_data if @embed_enabled.nil?
257
- channel_id = channel ? channel.resolve_id : @embed_channel_id
258
- response = JSON.parse(API::Server.modify_embed(@bot.token, @id, enabled, channel_id, reason))
259
- @embed_enabled = response['enabled']
260
- @embed_channel_id = response['channel_id']
243
+ def modify_widget(enabled, channel, reason = nil)
244
+ cache_widget_data if @widget_enabled.nil?
245
+ channel_id = channel ? channel.resolve_id : @widget_channel_id
246
+ response = JSON.parse(API::Server.modify_widget(@bot.token, @id, enabled, channel_id, reason))
247
+ @widget_enabled = response['enabled']
248
+ @widget_channel_id = response['channel_id']
261
249
  end
262
-
263
- alias_method :modify_widget, :modify_embed
250
+ alias_method :modify_embed, :modify_widget
264
251
 
265
252
  # @param include_idle [true, false] Whether to count idle members as online.
266
253
  # @param include_bots [true, false] Whether to include bot accounts in the count.
@@ -444,7 +431,7 @@ module Discordrb
444
431
  # @!visibility private
445
432
  def delete_member(user_id)
446
433
  @members.delete(user_id)
447
- @member_count -= 1
434
+ @member_count -= 1 unless @member_count <= 0
448
435
  end
449
436
 
450
437
  # Checks whether a member is cached
@@ -586,7 +573,7 @@ module Discordrb
586
573
  # The amount of emoji the server can have, based on its current Nitro Boost Level.
587
574
  # @return [Integer] the max amount of emoji
588
575
  def max_emoji
589
- case @level
576
+ case @boost_level
590
577
  when 1
591
578
  100
592
579
  when 2
@@ -598,9 +585,13 @@ module Discordrb
598
585
  end
599
586
  end
600
587
 
588
+ # Retrieve banned users from this server.
589
+ # @param limit [Integer] Number of users to return (up to maximum 1000, default 1000).
590
+ # @param before_id [Integer] Consider only users before given user id.
591
+ # @param after_id [Integer] Consider only users after given user id.
601
592
  # @return [Array<ServerBan>] a list of banned users on this server and the reason they were banned.
602
- def bans
603
- response = JSON.parse(API::Server.bans(@bot.token, @id))
593
+ def bans(limit: nil, before_id: nil, after_id: nil)
594
+ response = JSON.parse(API::Server.bans(@bot.token, @id, limit, before_id, after_id))
604
595
  response.map do |e|
605
596
  ServerBan.new(self, User.new(e['user'], @bot), e['reason'])
606
597
  end
@@ -628,11 +619,12 @@ module Discordrb
628
619
  API::Server.remove_member(@bot.token, @id, user.resolve_id, reason)
629
620
  end
630
621
 
631
- # Forcibly moves a user into a different voice channel. Only works if the bot has the permission needed.
622
+ # Forcibly moves a user into a different voice channel.
623
+ # Only works if the bot has the permission needed and if the user is already connected to some voice channel on this server.
632
624
  # @param user [User, String, Integer] The user to move.
633
- # @param channel [Channel, String, Integer] The voice channel to move into.
625
+ # @param channel [Channel, String, Integer, nil] The voice channel to move into. (If nil, the user is disconnected from the voice channel)
634
626
  def move(user, channel)
635
- API::Server.update_member(@bot.token, @id, user.resolve_id, channel_id: channel.resolve_id)
627
+ API::Server.update_member(@bot.token, @id, user.resolve_id, channel_id: channel&.resolve_id)
636
628
  end
637
629
 
638
630
  # Deletes this server. Be aware that this is permanent and impossible to undo, so be careful!
@@ -805,19 +797,16 @@ module Discordrb
805
797
  # Processes a GUILD_MEMBERS_CHUNK packet, specifically the members field
806
798
  # @note For internal use only
807
799
  # @!visibility private
808
- def process_chunk(members)
800
+ def process_chunk(members, chunk_index, chunk_count)
809
801
  process_members(members)
810
- @processed_chunk_members += members.length
811
- LOGGER.debug("Processed one chunk on server #{@id} - length #{members.length}")
802
+ LOGGER.debug("Processed chunk #{chunk_index + 1}/#{chunk_count} server #{@id} - index #{chunk_index} - length #{members.length}")
812
803
 
813
- # Don't bother with the rest of the method if it's not truly the last packet
814
- return unless @processed_chunk_members == @member_count
804
+ return if chunk_index + 1 < chunk_count
815
805
 
816
806
  LOGGER.debug("Finished chunking server #{@id}")
817
807
 
818
808
  # Reset everything to normal
819
809
  @chunked = true
820
- @processed_chunk_members = 0
821
810
  end
822
811
 
823
812
  # @return [Channel, nil] the AFK voice channel of this server, or `nil` if none is set.
@@ -842,17 +831,30 @@ module Discordrb
842
831
 
843
832
  afk_channel_id = new_data[:afk_channel_id] || new_data['afk_channel_id'] || @afk_channel
844
833
  @afk_channel_id = afk_channel_id.nil? ? nil : afk_channel_id.resolve_id
845
- embed_channel_id = new_data[:embed_channel_id] || new_data['embed_channel_id'] || @embed_channel
846
- @embed_channel_id = embed_channel_id.nil? ? nil : embed_channel_id.resolve_id
834
+ widget_channel_id = new_data[:widget_channel_id] || new_data['widget_channel_id'] || @widget_channel
835
+ @widget_channel_id = widget_channel_id.nil? ? nil : widget_channel_id.resolve_id
847
836
  system_channel_id = new_data[:system_channel_id] || new_data['system_channel_id'] || @system_channel
848
837
  @system_channel_id = system_channel_id.nil? ? nil : system_channel_id.resolve_id
849
838
 
850
- @embed_enabled = new_data[:embed_enabled] || new_data['embed_enabled']
839
+ @widget_enabled = new_data[:widget_enabled] || new_data['widget_enabled']
851
840
  @splash = new_data[:splash_id] || new_data['splash_id'] || @splash_id
852
841
 
853
842
  @verification_level = new_data[:verification_level] || new_data['verification_level'] || @verification_level
854
843
  @explicit_content_filter = new_data[:explicit_content_filter] || new_data['explicit_content_filter'] || @explicit_content_filter
855
844
  @default_message_notifications = new_data[:default_message_notifications] || new_data['default_message_notifications'] || @default_message_notifications
845
+
846
+ @large = new_data.key?('large') ? new_data['large'] : @large
847
+ @member_count = new_data['member_count'] || @member_count || 0
848
+ @splash_id = new_data['splash'] || @splash_id
849
+ @banner_id = new_data['banner'] || @banner_id
850
+ @features = new_data['features'] ? new_data['features'].map { |element| element.downcase.to_sym } : @features || []
851
+
852
+ process_channels(new_data['channels']) if new_data['channels']
853
+ process_roles(new_data['roles']) if new_data['roles']
854
+ process_emoji(new_data['emojis']) if new_data['emojis']
855
+ process_members(new_data['members']) if new_data['members']
856
+ process_presences(new_data['presences']) if new_data['presences']
857
+ process_voice_states(new_data['voice_states']) if new_data['voice_states']
856
858
  end
857
859
 
858
860
  # Adds a channel to this server's cache
@@ -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,21 @@ 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
+ # Utility function to get Discord's display name of a user not in server
53
+ # @return [String] the name the user displays as (global_name if they have one, username otherwise)
54
+ def display_name
55
+ global_name || username
56
+ end
57
+
24
58
  # Utility function to mention users in messages
25
59
  # @return [String] the mention code in the form of <@id>
26
60
  def mention
@@ -29,18 +63,37 @@ module Discordrb
29
63
 
30
64
  # Utility function to get Discord's distinct representation of a user, i.e. username + discriminator
31
65
  # @return [String] distinct representation of user
66
+ # TODO: Maybe change this method again after discriminator removal ?
32
67
  def distinct
33
- "#{@username}##{@discriminator}"
68
+ if @discriminator && @discriminator != '0'
69
+ "#{@username}##{@discriminator}"
70
+ else
71
+ @username.to_s
72
+ end
34
73
  end
35
74
 
36
75
  # Utility function to get a user's avatar URL.
37
76
  # @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
77
  # @return [String] the URL to the avatar image.
78
+ # TODO: Maybe change this method again after discriminator removal ?
39
79
  def avatar_url(format = nil)
40
- return API::User.default_avatar(@discriminator) unless @avatar_id
80
+ unless @avatar_id
81
+ return API::User.default_avatar(@discriminator, legacy: true) if @discriminator && @discriminator != '0'
82
+
83
+ return API::User.default_avatar(@id)
84
+ end
41
85
 
42
86
  API::User.avatar_url(@id, @avatar_id, format)
43
87
  end
88
+
89
+ # @return [Integer] the public flags on a user's account
90
+ attr_reader :public_flags
91
+
92
+ FLAGS.each do |name, value|
93
+ define_method("#{name}?") do
94
+ (@public_flags & value).positive?
95
+ end
96
+ end
44
97
  end
45
98
 
46
99
  # User on Discord, including internal data like discriminators
@@ -63,15 +116,20 @@ module Discordrb
63
116
  @bot = bot
64
117
 
65
118
  @username = data['username']
119
+ @global_name = data['global_name']
66
120
  @id = data['id'].to_i
67
121
  @discriminator = data['discriminator']
68
122
  @avatar_id = data['avatar']
69
123
  @roles = {}
70
124
  @activities = Discordrb::ActivitySet.new
125
+ @public_flags = data['public_flags'] || 0
71
126
 
72
127
  @bot_account = false
73
128
  @bot_account = true if data['bot']
74
129
 
130
+ @webhook_account = false
131
+ @webhook_account = true if data['_webhook']
132
+
75
133
  @status = :offline
76
134
  @client_status = process_client_status(data['client_status'])
77
135
  end
@@ -109,13 +167,20 @@ module Discordrb
109
167
  pm.send_file(file, caption: caption, filename: filename, spoiler: spoiler)
110
168
  end
111
169
 
112
- # Set the user's name
170
+ # Set the user's username
113
171
  # @note for internal use only
114
172
  # @!visibility private
115
173
  def update_username(username)
116
174
  @username = username
117
175
  end
118
176
 
177
+ # Set the user's global_name
178
+ # @note For internal use only.
179
+ # @!visibility private
180
+ def update_global_name(global_name)
181
+ @global_name = global_name
182
+ end
183
+
119
184
  # Set the user's presence data
120
185
  # @note for internal use only
121
186
  # @!visibility private
@@ -154,14 +219,9 @@ module Discordrb
154
219
  @bot.profile.id == @id
155
220
  end
156
221
 
157
- # @return [true, false] whether this user is a fake user for a webhook message
158
- def webhook?
159
- @discriminator == Message::ZERO_DISCRIM
160
- end
161
-
162
222
  # @!visibility private
163
223
  def process_client_status(client_status)
164
- (client_status || {}).map { |k, v| [k.to_sym, v.to_sym] }.to_h
224
+ (client_status || {}).to_h { |k, v| [k.to_sym, v.to_sym] }
165
225
  end
166
226
 
167
227
  # @!method offline?
@@ -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
@@ -81,13 +84,13 @@ module Discordrb
81
84
  def update(data)
82
85
  # Only pass a value for avatar if the key is defined as sending nil will delete the
83
86
  data[:avatar] = avatarise(data[:avatar]) if data.key?(:avatar)
84
- data[:channel_id] = data[:channel].resolve_id
87
+ data[:channel_id] = data[:channel]&.resolve_id
85
88
  data.delete(:channel)
86
- update_webhook(data)
89
+ update_webhook(**data)
87
90
  end
88
91
 
89
92
  # Deletes the webhook.
90
- # @param reason [String] The reason the invite is being deleted.
93
+ # @param reason [String] The reason the webhook is being deleted.
91
94
  def delete(reason = nil)
92
95
  if token?
93
96
  API::Webhook.token_delete_webhook(@token, @id, reason)
@@ -96,10 +99,100 @@ module Discordrb
96
99
  end
97
100
  end
98
101
 
102
+ # Execute a webhook.
103
+ # @param content [String] The content of the message. May be 2000 characters long at most.
104
+ # @param username [String] The username the webhook will display as. If this is not set, the default username set in the webhook's settings.
105
+ # @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
106
+ # @param tts [true, false] Whether this message should use TTS or not. By default, it doesn't.
107
+ # @param file [File] File to be sent together with the message. Mutually exclusive with embeds; a webhook message can contain
108
+ # either a file to be sent or embeds.
109
+ # @param embeds [Array<Webhooks::Embed, Hash>] Embeds to attach to this message.
110
+ # @param allowed_mentions [AllowedMentions, Hash] Mentions that are allowed to ping in the `content`.
111
+ # @param wait [true, false] Whether Discord should wait for the message to be successfully received by clients, or
112
+ # whether it should return immediately after sending the message. If `true` a {Message} object will be returned.
113
+ # @yield [builder] Gives the builder to the block to add additional steps, or to do the entire building process.
114
+ # @yieldparam builder [Builder] The builder given as a parameter which is used as the initial step to start from.
115
+ # @example Execute the webhook with kwargs
116
+ # client.execute(
117
+ # content: 'Testing',
118
+ # username: 'discordrb',
119
+ # embeds: [
120
+ # { timestamp: Time.now.iso8601, title: 'testing', image: { url: 'https://i.imgur.com/PcMltU7.jpg' } }
121
+ # ])
122
+ # @example Execute the webhook with an already existing builder
123
+ # builder = Discordrb::Webhooks::Builder.new # ...
124
+ # client.execute(builder)
125
+ # @example Execute the webhook by building a new message
126
+ # client.execute do |builder|
127
+ # builder.content = 'Testing'
128
+ # builder.username = 'discordrb'
129
+ # builder.add_embed do |embed|
130
+ # embed.timestamp = Time.now
131
+ # embed.title = 'Testing'
132
+ # embed.image = Discordrb::Webhooks::EmbedImage.new(url: 'https://i.imgur.com/PcMltU7.jpg')
133
+ # end
134
+ # end
135
+ # @return [Message, nil] If `wait` is `true`, a {Message} will be returned. Otherwise this method will return `nil`.
136
+ # @note This is only available to webhooks with publically exposed tokens. This excludes channel follow webhooks and webhooks retrieved
137
+ # via the audit log.
138
+ def execute(content: nil, username: nil, avatar_url: nil, tts: nil, file: nil, embeds: nil, allowed_mentions: nil, wait: true, builder: nil, components: nil)
139
+ raise Discordrb::Errors::UnauthorizedWebhook unless @token
140
+
141
+ params = { content: content, username: username, avatar_url: avatar_url, tts: tts, file: file, embeds: embeds, allowed_mentions: allowed_mentions }
142
+
143
+ builder ||= Webhooks::Builder.new
144
+ view = Webhooks::View.new
145
+
146
+ yield(builder, view) if block_given?
147
+
148
+ data = builder.to_json_hash.merge(params.compact)
149
+ components ||= view
150
+
151
+ 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)
152
+
153
+ Message.new(JSON.parse(resp), @bot) if wait
154
+ end
155
+
156
+ # Delete a message created by this webhook.
157
+ # @param message [Message, String, Integer] The ID of the message to delete.
158
+ def delete_message(message)
159
+ raise Discordrb::Errors::UnauthorizedWebhook unless @token
160
+
161
+ API::Webhook.token_delete_message(@token, @id, message.resolve_id)
162
+ end
163
+
164
+ # Edit a message created by this webhook.
165
+ # @param message [Message, String, Integer] The ID of the message to edit.
166
+ # @param content [String] The content of the message. May be 2000 characters long at most.
167
+ # @param embeds [Array<Webhooks::Embed, Hash>] Embeds to be attached to the message.
168
+ # @param allowed_mentions [AllowedMentions, Hash] Mentions that are allowed to ping in the `content`.
169
+ # @param builder [Builder, nil] The builder to start out with, or nil if one should be created anew.
170
+ # @yield [builder] Gives the builder to the block to add additional steps, or to do the entire building process.
171
+ # @yieldparam builder [Webhooks::Builder] The builder given as a parameter which is used as the initial step to start from.
172
+ # @return [Message] The updated message.
173
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
174
+ # @note When editing `allowed_mentions`, it will update visually in the client but not alert the user with a notification.
175
+ def edit_message(message, content: nil, embeds: nil, allowed_mentions: nil, builder: nil, components: nil)
176
+ raise Discordrb::Errors::UnauthorizedWebhook unless @token
177
+
178
+ params = { content: content, embeds: embeds, allowed_mentions: allowed_mentions }.compact
179
+
180
+ builder ||= Webhooks::Builder.new
181
+ view ||= Webhooks::View.new
182
+
183
+ yield(builder, view) if block_given?
184
+
185
+ data = builder.to_json_hash.merge(params.compact)
186
+ components ||= view
187
+
188
+ resp = API::Webhook.token_edit_message(@token, @id, message.resolve_id, data[:content], data[:embeds], data[:allowed_mentions], components.to_a)
189
+ Message.new(JSON.parse(resp), @bot)
190
+ end
191
+
99
192
  # Utility function to get a webhook's avatar URL.
100
193
  # @return [String] the URL to the avatar image
101
194
  def avatar_url
102
- return API::User.default_avatar unless @avatar
195
+ return API::User.default_avatar(@id) unless @avatar
103
196
 
104
197
  API::User.avatar_url(@id, @avatar)
105
198
  end
@@ -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,5 @@ 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'
@@ -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'] ? "#{sub_err['code']}: " : nil}#{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] || UnknownError
62
103
  end
63
104
 
64
105
  # Used when Discord doesn't provide a more specific code
@@ -31,7 +31,7 @@ module Discordrb::Events
31
31
 
32
32
  def initialize(data, bot)
33
33
  @bot = bot
34
- @channel = bot.channel(data['id'].to_i)
34
+ @channel = data.is_a?(Discordrb::Channel) ? data : bot.channel(data['id'].to_i)
35
35
  end
36
36
  end
37
37