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
@@ -5,17 +5,80 @@ module Discordrb
5
5
  class Message
6
6
  include IDObject
7
7
 
8
+ # Map of message flags.
9
+ FLAGS = {
10
+ crossposted: 1 << 0,
11
+ crosspost: 1 << 1,
12
+ suppress_embeds: 1 << 2,
13
+ source_message_deleted: 1 << 3,
14
+ urgent: 1 << 4,
15
+ thread: 1 << 5,
16
+ ephemeral: 1 << 6,
17
+ loading: 1 << 7,
18
+ failed_to_mention_roles: 1 << 8,
19
+ suppress_notifications: 1 << 12,
20
+ voice_message: 1 << 13,
21
+ snapshot: 1 << 14,
22
+ uikit_components: 1 << 15
23
+ }.freeze
24
+
25
+ # Map of message types.
26
+ TYPES = {
27
+ default: 0,
28
+ recipient_add: 1,
29
+ recipient_remove: 2,
30
+ call: 3,
31
+ channel_name_change: 4,
32
+ channel_icon_change: 5,
33
+ channel_pinned_message: 6,
34
+ server_member_join: 7,
35
+ server_boost: 8,
36
+ server_boost_tier_one: 9,
37
+ server_boost_tier_two: 10,
38
+ server_boost_tier_three: 11,
39
+ channel_follow_add: 12,
40
+ server_discovery_disqualified: 14,
41
+ server_discovery_requalified: 15,
42
+ server_discovery_grace_period_initial_warning: 16,
43
+ server_discovery_grace_period_final_warning: 17,
44
+ thread_created: 18,
45
+ reply: 19,
46
+ chat_input_command: 20,
47
+ thread_starter_message: 21,
48
+ server_invite_reminder: 22,
49
+ context_menu_command: 23,
50
+ automod_action: 24,
51
+ role_subscription_purchase: 25,
52
+ interaction_premium_upsell: 26,
53
+ stage_start: 27,
54
+ stage_end: 28,
55
+ stage_speaker: 29,
56
+ stage_raise_hand: 30,
57
+ stage_topic: 31,
58
+ server_application_premium_subscription: 32,
59
+ server_incident_alert_mode_enabled: 36,
60
+ server_incident_alert_mode_disabled: 37,
61
+ server_incident_report_raid: 38,
62
+ server_incident_report_false_alarm: 39,
63
+ purchase_notification: 44,
64
+ poll_result: 46,
65
+ changelog: 47,
66
+ server_join_request_accepted: 52,
67
+ server_join_request_rejected: 53,
68
+ server_join_request_withdrawn: 54,
69
+ report_to_mod_deleted_message: 58,
70
+ report_to_mod_timeout_user: 59,
71
+ report_to_mod_kick_user: 60,
72
+ report_to_mod_ban_user: 61,
73
+ report_to_mod_closed_report: 62,
74
+ server_emoji_added: 63
75
+ }.freeze
76
+
8
77
  # @return [String] the content of this message.
9
78
  attr_reader :content
10
79
  alias_method :text, :content
11
80
  alias_method :to_s, :content
12
81
 
13
- # @return [Member, User] the user that sent this message. (Will be a {Member} most of the time, it should only be a
14
- # {User} for old messages when the author has left the server since then)
15
- attr_reader :author
16
- alias_method :user, :author
17
- alias_method :writer, :author
18
-
19
82
  # @return [Channel] the channel in which this message was sent.
20
83
  attr_reader :channel
21
84
 
@@ -61,14 +124,32 @@ module Discordrb
61
124
  attr_reader :pinned
62
125
  alias_method :pinned?, :pinned
63
126
 
127
+ # @return [Integer] what the type of the message is
128
+ attr_reader :type
129
+
64
130
  # @return [Server, nil] the server in which this message was sent.
65
131
  attr_reader :server
66
132
 
67
133
  # @return [Integer, nil] the webhook ID that sent this message, or `nil` if it wasn't sent through a webhook.
68
134
  attr_reader :webhook_id
69
135
 
70
- # The discriminator that webhook user accounts have.
71
- ZERO_DISCRIM = '0000'
136
+ # @return [Array<Component>] Interaction components for this message.
137
+ attr_reader :components
138
+
139
+ # @return [Integer] flags set on the message.
140
+ attr_reader :flags
141
+
142
+ # @return [Channel, nil] The thread that was started from this message, or nil.
143
+ attr_reader :thread
144
+
145
+ # @return [Time, nil] the time at when this message was pinned. Only present on messages fetched via {Channel#pins}.
146
+ attr_reader :pinned_at
147
+
148
+ # @return [Call, nil] the call in a private channel that prompted this message.
149
+ attr_reader :call
150
+
151
+ # @return [Array<Snapshot>] the message snapshots included in this message.
152
+ attr_reader :snapshots
72
153
 
73
154
  # @!visibility private
74
155
  def initialize(data, bot)
@@ -76,6 +157,7 @@ module Discordrb
76
157
  @content = data['content']
77
158
  @channel = bot.channel(data['channel_id'].to_i)
78
159
  @pinned = data['pinned']
160
+ @type = data['type']
79
161
  @tts = data['tts']
80
162
  @nonce = data['nonce']
81
163
  @mention_everyone = data['mention_everyone']
@@ -85,36 +167,23 @@ module Discordrb
85
167
 
86
168
  @server = @channel.server
87
169
 
88
- @author = if data['author']
89
- if data['author']['discriminator'] == ZERO_DISCRIM
90
- # This is a webhook user! It would be pointless to try to resolve a member here, so we just create
91
- # a User and return that instead.
92
- Discordrb::LOGGER.debug("Webhook user: #{data['author']['id']}")
93
- User.new(data['author'], @bot)
94
- elsif @channel.private?
95
- # Turn the message user into a recipient - we can't use the channel recipient
96
- # directly because the bot may also send messages to the channel
97
- Recipient.new(bot.user(data['author']['id'].to_i), @channel, bot)
98
- else
99
- member = @channel.server.member(data['author']['id'].to_i)
100
-
101
- if member
102
- member.update_data(data['member']) if data['member']
103
- else
104
- Discordrb::LOGGER.debug("Member with ID #{data['author']['id']} not cached (possibly left the server).")
105
- member = if data['member']
106
- member_data = data['author'].merge(data['member'])
107
- Member.new(member_data, bot)
108
- else
109
- @bot.ensure_user(data['author'])
110
- end
111
- end
112
-
113
- member
114
- end
115
- end
116
-
117
- @webhook_id = data['webhook_id'].to_i if data['webhook_id']
170
+ @webhook_id = data['webhook_id']&.to_i
171
+
172
+ if data['author']
173
+ if @webhook_id
174
+ # This is a webhook user! It would be pointless to try to resolve a member here, so we just create
175
+ # a User and return that instead.
176
+ Discordrb::LOGGER.debug("Webhook user: #{data['author']['id']}")
177
+ @author = User.new(data['author'].merge({ '_webhook' => true }), @bot)
178
+ elsif @channel.private?
179
+
180
+ # Turn the message user into a recipient - we can't use the channel recipient
181
+ # directly because the bot may also send messages to the channel
182
+ @author = Recipient.new(bot.user(data['author']['id'].to_i), @channel, bot)
183
+ else
184
+ @author_id = data['author']['id'].to_i
185
+ end
186
+ end
118
187
 
119
188
  @timestamp = Time.parse(data['timestamp']) if data['timestamp']
120
189
  @edited_timestamp = data['edited_timestamp'].nil? ? nil : Time.parse(data['edited_timestamp'])
@@ -149,43 +218,81 @@ module Discordrb
149
218
 
150
219
  @embeds = []
151
220
  @embeds = data['embeds'].map { |e| Embed.new(e, self) } if data['embeds']
221
+
222
+ @components = []
223
+ @components = data['components'].map { |component_data| Components.from_data(component_data, @bot) } if data['components']
224
+
225
+ @flags = data['flags'] || 0
226
+
227
+ @thread = data['thread'] ? @bot.ensure_channel(data['thread'], @server) : nil
228
+
229
+ @pinned_at = data['pinned_at'] ? Time.parse(data['pinned_at']) : nil
230
+
231
+ @call = data['call'] ? Call.new(data['call'], @bot) : nil
232
+
233
+ @snapshots = data['message_snapshots']&.map { |snapshot| Snapshot.new(snapshot['message'], @bot) } || []
234
+ end
235
+
236
+ # @return [Member, User] the user that sent this message. (Will be a {Member} most of the time, it should only be a
237
+ # {User} for old messages when the author has left the server since then)
238
+ def author
239
+ return @author if @author
240
+
241
+ if @channel.server
242
+ @author = @channel.server.member(@author_id)
243
+ Discordrb::LOGGER.debug("Member with ID #{@author_id} not cached (possibly left the server).") if @author.nil?
244
+ end
245
+
246
+ @author ||= @bot.user(@author_id)
152
247
  end
153
248
 
249
+ alias_method :user, :author
250
+ alias_method :writer, :author
251
+
154
252
  # Replies to this message with the specified content.
155
253
  # @deprecated Please use {#respond}.
254
+ # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
255
+ # @return (see #respond)
156
256
  # @see Channel#send_message
157
257
  def reply(content)
158
258
  @channel.send_message(content)
159
259
  end
160
260
 
161
- # Sends a message to this channel.
261
+ # Responds to this message as an inline reply.
162
262
  # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
163
263
  # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
164
264
  # @param embed [Hash, Discordrb::Webhooks::Embed, nil] The rich embed to append to this message.
165
265
  # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
166
266
  # @param allowed_mentions [Hash, Discordrb::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
167
267
  # @param mention_user [true, false] Whether the user that is being replied to should be pinged by the reply.
168
- # @return [Message] the message that was sent.
169
- def reply!(content, tts: false, embed: nil, attachments: nil, allowed_mentions: {}, mention_user: false)
268
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
269
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
270
+ # @return (see #respond)
271
+ def reply!(content, tts: false, embed: nil, attachments: nil, allowed_mentions: {}, mention_user: false, components: nil, flags: 0)
170
272
  allowed_mentions = { parse: [] } if allowed_mentions == false
171
273
  allowed_mentions = allowed_mentions.to_hash.transform_keys(&:to_sym)
172
274
  allowed_mentions[:replied_user] = mention_user
173
275
 
174
- respond(content, tts, embed, attachments, allowed_mentions, self)
276
+ respond(content, tts, embed, attachments, allowed_mentions, self, components, flags)
175
277
  end
176
278
 
177
279
  # (see Channel#send_message)
178
- def respond(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil)
179
- @channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference)
280
+ def respond(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0)
281
+ @channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference, components, flags)
180
282
  end
181
283
 
182
284
  # Edits this message to have the specified content instead.
183
285
  # You can only edit your own messages.
184
286
  # @param new_content [String] the new content the message should have.
185
- # @param new_embed [Hash, Discordrb::Webhooks::Embed, nil] The new embed the message should have. If `nil` the message will be changed to have no embed.
287
+ # @param new_embeds [Hash, Discordrb::Webhooks::Embed, Array<Hash>, Array<Discordrb::Webhooks::Embed>, nil] The new embeds the message should have. If `nil` the message will be changed to have no embeds.
288
+ # @param new_components [View, Array<Hash>] The new components the message should have. If `nil` the message will be changed to have no components.
289
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) can be edited.
186
290
  # @return [Message] the resulting message.
187
- def edit(new_content, new_embed = nil)
188
- response = API::Channel.edit_message(@bot.token, @channel.id, @id, new_content, [], new_embed ? new_embed.to_hash : nil)
291
+ def edit(new_content, new_embeds = nil, new_components = nil, flags = 0)
292
+ new_embeds = (new_embeds.instance_of?(Array) ? new_embeds.map(&:to_hash) : [new_embeds&.to_hash]).compact
293
+ new_components = new_components.to_a
294
+
295
+ response = API::Channel.edit_message(@bot.token, @channel.id, @id, new_content, [], new_embeds, new_components, flags)
189
296
  Message.new(JSON.parse(response), @bot)
190
297
  end
191
298
 
@@ -209,6 +316,12 @@ module Discordrb
209
316
  nil
210
317
  end
211
318
 
319
+ # Crossposts a message in a news channel.
320
+ def crosspost
321
+ response = API::Channel.crosspost_message(@bot.token, @channel.id, @id)
322
+ Message.new(JSON.parse(response), @bot)
323
+ end
324
+
212
325
  # Add an {Await} for a message with the same user and channel.
213
326
  # @see Bot#add_await
214
327
  # @deprecated Will be changed to blocking behavior in v4.0. Use {#await!} instead.
@@ -222,6 +335,19 @@ module Discordrb
222
335
  @bot.add_await!(Discordrb::Events::MessageEvent, { from: @author.id, in: @channel.id }.merge(attributes), &block)
223
336
  end
224
337
 
338
+ # Add an {Await} for a reaction to be added on this message.
339
+ # @see Bot#add_await
340
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#await_reaction!} instead.
341
+ def await_reaction(key, attributes = {}, &block)
342
+ @bot.add_await(key, Discordrb::Events::ReactionAddEvent, { message: @id }.merge(attributes), &block)
343
+ end
344
+
345
+ # Add a blocking {Await} for a reaction to be added on this message.
346
+ # @see Bot#add_await!
347
+ def await_reaction!(attributes = {}, &block)
348
+ @bot.add_await!(Discordrb::Events::ReactionAddEvent, { message: @id }.merge(attributes), &block)
349
+ end
350
+
225
351
  # @return [true, false] whether this message was sent by the current {Bot}.
226
352
  def from_bot?
227
353
  @author&.current_bot?
@@ -258,6 +384,25 @@ module Discordrb
258
384
  @reactions.select(&:me)
259
385
  end
260
386
 
387
+ # Removes embeds from the message
388
+ # @return [Message] the resulting message.
389
+ def suppress_embeds
390
+ flags = @flags | (1 << 2)
391
+ response = API::Channel.edit_message(@bot.token, @channel.id, @id, :undef, :undef, :undef, :undef, flags)
392
+ Message.new(JSON.parse(response), @bot)
393
+ end
394
+
395
+ # Check if this message mentions a specific user or role.
396
+ # @param target [Role, User, Member, Integer, String] The mention to match against.
397
+ # @return [true, false] whether or not this message mentions the target.
398
+ def mentions?(target)
399
+ mentions = (@mentions + role_mentions)
400
+
401
+ mentions << server if @mention_everyone
402
+
403
+ mentions.any?(target.resolve_id)
404
+ end
405
+
261
406
  # Reacts to a message.
262
407
  # @param reaction [String, #to_reaction] the unicode emoji or {Emoji}
263
408
  def create_reaction(reaction)
@@ -276,14 +421,38 @@ module Discordrb
276
421
  # @return [Array<User>] the users who used this reaction
277
422
  def reacted_with(reaction, limit: 100)
278
423
  reaction = reaction.to_reaction if reaction.respond_to?(:to_reaction)
424
+ reaction = reaction.to_s if reaction.respond_to?(:to_s)
425
+
426
+ get_reactions = proc do |fetch_limit, after_id = nil|
427
+ resp = API::Channel.get_reactions(@bot.token, @channel.id, @id, reaction, nil, after_id, fetch_limit)
428
+ JSON.parse(resp).map { |d| User.new(d, @bot) }
429
+ end
430
+
431
+ # Can be done without pagination
432
+ return get_reactions.call(limit) if limit && limit <= 100
433
+
279
434
  paginator = Paginator.new(limit, :down) do |last_page|
280
- after_id = last_page.last.id if last_page
281
- last_page = JSON.parse(API::Channel.get_reactions(@bot.token, @channel.id, @id, reaction, nil, after_id, limit))
282
- last_page.map { |d| User.new(d, @bot) }
435
+ if last_page && last_page.count < 100
436
+ []
437
+ else
438
+ get_reactions.call(100, last_page&.last&.id)
439
+ end
283
440
  end
441
+
284
442
  paginator.to_a
285
443
  end
286
444
 
445
+ # Returns a hash of all reactions to a message as keys and the users that reacted to it as values.
446
+ # @param limit [Integer] the limit of how many users to retrieve per distinct reaction emoji. `nil` will return all users
447
+ # @example Get all the users that reacted to a message for a giveaway.
448
+ # giveaway_participants = message.all_reaction_users
449
+ # @return [Hash<String => Array<User>>] A hash mapping the string representation of a
450
+ # reaction to an array of users.
451
+ def all_reaction_users(limit: 100)
452
+ all_reactions = @reactions.map { |r| { r.to_s => reacted_with(r, limit: limit) } }
453
+ all_reactions.reduce({}, :merge)
454
+ end
455
+
287
456
  # Deletes a reaction made by a user on this message.
288
457
  # @param user [User, String, Integer] the user or user ID who used this reaction
289
458
  # @param reaction [String, #to_reaction] the reaction to remove
@@ -328,7 +497,65 @@ module Discordrb
328
497
  return nil unless @message_reference
329
498
 
330
499
  referenced_channel = @bot.channel(@message_reference['channel_id'])
331
- @referenced_message = referenced_channel.message(@message_reference['message_id'])
500
+ @referenced_message = referenced_channel&.message(@message_reference['message_id'])
501
+ end
502
+
503
+ # @return [Array<Components::Button>]
504
+ def buttons
505
+ results = @components.collect do |component|
506
+ case component
507
+ when Components::Button
508
+ component
509
+ when Components::ActionRow
510
+ component.buttons
511
+ end
512
+ end
513
+
514
+ results.flatten.compact
515
+ end
516
+
517
+ # to_message -> self or message
518
+ # @return [Discordrb::Message]
519
+ def to_message
520
+ self
521
+ end
522
+
523
+ alias_method :message, :to_message
524
+
525
+ FLAGS.each do |name, value|
526
+ define_method("#{name}?") do
527
+ @flags.anybits?(value)
528
+ end
529
+ end
530
+
531
+ TYPES.each do |name, value|
532
+ define_method("#{name}?") do
533
+ @type == value
534
+ end
535
+ end
536
+
537
+ # Convert this message to a hash that can be used to reference this message in a forward or a reply.
538
+ # @param type [Integer, Symbol] The reference type to set. Can either be one of `:reply` or `:forward`.
539
+ # @param must_exist [true, false] Whether to raise an error if this message was deleted when sending it.
540
+ # @return [Hash] the message as a hash representation that can be used in a forwarded message or a reply.
541
+ def to_reference(type: :reply, must_exist: true)
542
+ type = (type == :reply ? 0 : 1) if type.is_a?(Symbol)
543
+
544
+ { type: type, message_id: @id, channel_id: @channel.id, fail_if_not_exists: must_exist }
545
+ end
546
+
547
+ # Forward this message to another channel.
548
+ # @param channel [Integer, String, Channel] The target channel to forward this message to.
549
+ # @param must_exist [true, false] Whether to raise an error if this message was deleted when sending it.
550
+ # @param timeout [Float, nil] The amount of time in seconds after which the message sent will be deleted.
551
+ # @param flags [Integer, Symbol, Array<Integer, Symbol>] The message flags to set on the forwarded message.
552
+ # @param nonce [String, Integer, nil] The 25 character optional nonce that should be used when forwarding this message.
553
+ # @param enforce_nonce [true, false] Whether the provided nonce should be enforced and used for message de-duplication.
554
+ # @return [Message, nil] the message that was created from forwarding this one, or `nil` if this is a temporary message.
555
+ def forward(channel, must_exist: true, timeout: nil, flags: 0, nonce: nil, enforce_nonce: false)
556
+ reference = to_reference(type: :forward, must_exist: must_exist)
557
+
558
+ @bot.channel(channel).send_message!(reference: reference, timeout: timeout, flags: flags, nonce: nonce, enforce_nonce: enforce_nonce)
332
559
  end
333
560
  end
334
561
  end
@@ -4,6 +4,12 @@ module Discordrb
4
4
  # A permissions overwrite, when applied to channels describes additional
5
5
  # permissions a member needs to perform certain actions in context.
6
6
  class Overwrite
7
+ # Types of overwrites mapped to their API value.
8
+ TYPES = {
9
+ role: 0,
10
+ member: 1
11
+ }.freeze
12
+
7
13
  # @return [Integer] ID of the thing associated with this overwrite type
8
14
  attr_accessor :id
9
15
 
@@ -32,14 +38,14 @@ module Discordrb
32
38
  # @example Create an overwrite by ID and permissions bits
33
39
  # Overwrite.new(120571255635181568, type: 'member', allow: 1024, deny: 0)
34
40
  # @param object [Integer, #id] the ID or object this overwrite is for
35
- # @param type [String] the type of object this overwrite is for (only required if object is an Integer)
36
- # @param allow [Integer, Permissions] allowed permissions for this overwrite, by bits or a Permissions object
37
- # @param deny [Integer, Permissions] denied permissions for this overwrite, by bits or a Permissions object
41
+ # @param type [String, Symbol, Integer] the type of object this overwrite is for (only required if object is an Integer)
42
+ # @param allow [String, Integer, Permissions] allowed permissions for this overwrite, by bits or a Permissions object
43
+ # @param deny [String, Integer, Permissions] denied permissions for this overwrite, by bits or a Permissions object
38
44
  # @raise [ArgumentError] if type is not :member or :role
39
45
  def initialize(object = nil, type: nil, allow: 0, deny: 0)
40
46
  if type
41
- type = type.to_sym
42
- raise ArgumentError, 'Overwrite type must be :member or :role' unless (type != :member) || (type != :role)
47
+ type = TYPES.value?(type) ? TYPES.key(type) : type.to_sym
48
+ raise ArgumentError, 'Overwrite type must be :member or :role' unless type
43
49
  end
44
50
 
45
51
  @id = object.respond_to?(:id) ? object.id : object
@@ -59,7 +65,9 @@ module Discordrb
59
65
 
60
66
  # Comparison by attributes [:id, :type, :allow, :deny]
61
67
  def ==(other)
68
+ # rubocop:disable Lint/Void
62
69
  false unless other.is_a? Discordrb::Overwrite
70
+ # rubocop:enable Lint/Void
63
71
  id == other.id &&
64
72
  type == other.type &&
65
73
  allow == other.allow &&
@@ -71,7 +79,7 @@ module Discordrb
71
79
  def self.from_hash(data)
72
80
  new(
73
81
  data['id'].to_i,
74
- type: data['type'],
82
+ type: TYPES.key(data['type']),
75
83
  allow: Permissions.new(data['allow']),
76
84
  deny: Permissions.new(data['deny'])
77
85
  )
@@ -93,7 +101,7 @@ module Discordrb
93
101
  def to_hash
94
102
  {
95
103
  id: id,
96
- type: type,
104
+ type: TYPES[type],
97
105
  allow: allow.bits,
98
106
  deny: deny.bits
99
107
  }
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # A server tag that a user has chosen to display on their profile.
5
+ class PrimaryServer
6
+ # @return [Integer] the ID of the server this primary server is for.
7
+ attr_reader :server_id
8
+
9
+ # @return [Boolean] if the user is displaying the primary server's tag.
10
+ attr_reader :enabled
11
+ alias_method :enabled?, :enabled
12
+
13
+ # @return [String] the text of the primary server's tag. Limited to four characters.
14
+ attr_reader :name
15
+ alias_method :text, :name
16
+
17
+ # @return [String] the ID of the server tag's badge. can be used to generate a badge URL.
18
+ # @see #badge_url
19
+ attr_reader :badge_id
20
+
21
+ # @!visibility private
22
+ def initialize(data, bot)
23
+ @bot = bot
24
+ @server_id = data['identity_guild_id']&.to_i
25
+ @enabled = data['identity_enabled']
26
+ @name = data['tag']
27
+ @badge_id = data['badge']
28
+ end
29
+
30
+ # Get the server associated with this primary server.
31
+ # @return [Server] the server associated with this primary server.
32
+ # @raise [Discordrb::Errors::NoPermission] this can happen when the bot is not in the associated server.
33
+ def server
34
+ @bot.server(@server_id)
35
+ end
36
+
37
+ # Get the server preview associated with this primary server.
38
+ # @return [ServerPreview, nil] the server preview associated with this primary server, or `nil` if it can't be accessed.
39
+ def server_preview
40
+ @bot.server_preview(@server_id)
41
+ end
42
+
43
+ # Utility method to get a server tag's badge URL.
44
+ # @param format [String] the URL will default to `webp`. You can otherwise specify one of `jpg` or `png` to override this.
45
+ # @return [String] the URL to the server tag's badge image.
46
+ def badge_url(format = 'webp')
47
+ API.server_tag_badge_url(@server_id, @badge_id, format)
48
+ end
49
+
50
+ # Comparison based off of server ID.
51
+ # @return [true, false] if the other object is equal to this primary server.
52
+ def ==(other)
53
+ return false unless other.is_a?(PrimaryServer)
54
+
55
+ Discordrb.id_compare(other.server_id, @server_id)
56
+ end
57
+
58
+ alias_method :eql?, :==
59
+ end
60
+ end
@@ -22,13 +22,8 @@ module Discordrb
22
22
  # @param avatar [String, #read] A JPG file to be used as the avatar, either
23
23
  # something readable (e.g. File Object) or as a data URL.
24
24
  def avatar=(avatar)
25
- if avatar.respond_to? :read
26
- # Set the file to binary mode if supported, so we don't get problems with Windows
27
- avatar.binmode if avatar.respond_to?(:binmode)
28
-
29
- avatar_string = 'data:image/jpg;base64,'
30
- avatar_string += Base64.strict_encode64(avatar.read)
31
- update_profile_data(avatar: avatar_string)
25
+ if avatar.respond_to?(:read)
26
+ update_profile_data(avatar: Discordrb.encode64(avatar))
32
27
  else
33
28
  update_profile_data(avatar: avatar)
34
29
  end
@@ -16,10 +16,11 @@ module Discordrb
16
16
  # @return [String] the name or unicode representation of the emoji
17
17
  attr_reader :name
18
18
 
19
+ # @!visibility private
19
20
  def initialize(data)
20
21
  @count = data['count']
21
22
  @me = data['me']
22
- @id = data['emoji']['id'].nil? ? nil : data['emoji']['id'].to_i
23
+ @id = data['emoji']['id']&.to_i
23
24
  @name = data['emoji']['name']
24
25
  end
25
26
 
@@ -16,7 +16,7 @@ module Discordrb
16
16
  raise ArgumentError, 'Tried to create a recipient for a public channel!' unless @channel.private?
17
17
 
18
18
  @user = user
19
- super @user
19
+ super(@user)
20
20
 
21
21
  # Member attributes
22
22
  @mute = @deaf = @self_mute = @self_deaf = false