discordrb 3.4.3 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
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