onyxcord 1.1.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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/CONTRIBUTING.md +13 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  8. data/.github/pull_request_template.md +37 -0
  9. data/.github/workflows/ci.yml +78 -0
  10. data/.github/workflows/codeql.yml +65 -0
  11. data/.github/workflows/deploy.yml +54 -0
  12. data/.github/workflows/release.yml +51 -0
  13. data/.gitignore +16 -0
  14. data/.markdownlint.json +4 -0
  15. data/.overcommit.yml +7 -0
  16. data/.rspec +2 -0
  17. data/.rubocop.yml +129 -0
  18. data/.yardopts +1 -0
  19. data/CHANGELOG.md +0 -0
  20. data/Gemfile +7 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +305 -0
  23. data/Rakefile +17 -0
  24. data/bin/console +15 -0
  25. data/bin/setup +7 -0
  26. data/lib/onyxcord/allowed_mentions.rb +43 -0
  27. data/lib/onyxcord/api/application.rb +316 -0
  28. data/lib/onyxcord/api/channel.rb +700 -0
  29. data/lib/onyxcord/api/interaction.rb +67 -0
  30. data/lib/onyxcord/api/invite.rb +44 -0
  31. data/lib/onyxcord/api/server.rb +775 -0
  32. data/lib/onyxcord/api/user.rb +158 -0
  33. data/lib/onyxcord/api/webhook.rb +163 -0
  34. data/lib/onyxcord/api.rb +335 -0
  35. data/lib/onyxcord/await.rb +51 -0
  36. data/lib/onyxcord/bot.rb +1971 -0
  37. data/lib/onyxcord/cache.rb +326 -0
  38. data/lib/onyxcord/colour_rgb.rb +43 -0
  39. data/lib/onyxcord/commands/command_bot.rb +511 -0
  40. data/lib/onyxcord/commands/container.rb +112 -0
  41. data/lib/onyxcord/commands/events.rb +11 -0
  42. data/lib/onyxcord/commands/parser.rb +327 -0
  43. data/lib/onyxcord/commands/rate_limiter.rb +144 -0
  44. data/lib/onyxcord/configuration.rb +125 -0
  45. data/lib/onyxcord/container.rb +988 -0
  46. data/lib/onyxcord/data/activity.rb +271 -0
  47. data/lib/onyxcord/data/application.rb +341 -0
  48. data/lib/onyxcord/data/attachment.rb +91 -0
  49. data/lib/onyxcord/data/audit_logs.rb +438 -0
  50. data/lib/onyxcord/data/avatar_decoration.rb +26 -0
  51. data/lib/onyxcord/data/call.rb +22 -0
  52. data/lib/onyxcord/data/channel.rb +1355 -0
  53. data/lib/onyxcord/data/channel_tag.rb +69 -0
  54. data/lib/onyxcord/data/collectibles.rb +47 -0
  55. data/lib/onyxcord/data/component.rb +583 -0
  56. data/lib/onyxcord/data/embed.rb +258 -0
  57. data/lib/onyxcord/data/emoji.rb +123 -0
  58. data/lib/onyxcord/data/install_params.rb +24 -0
  59. data/lib/onyxcord/data/integration.rb +144 -0
  60. data/lib/onyxcord/data/interaction.rb +1141 -0
  61. data/lib/onyxcord/data/invite.rb +137 -0
  62. data/lib/onyxcord/data/member.rb +528 -0
  63. data/lib/onyxcord/data/message.rb +612 -0
  64. data/lib/onyxcord/data/message_activity.rb +41 -0
  65. data/lib/onyxcord/data/overwrite.rb +109 -0
  66. data/lib/onyxcord/data/poll.rb +365 -0
  67. data/lib/onyxcord/data/primary_server.rb +60 -0
  68. data/lib/onyxcord/data/profile.rb +79 -0
  69. data/lib/onyxcord/data/reaction.rb +64 -0
  70. data/lib/onyxcord/data/recipient.rb +34 -0
  71. data/lib/onyxcord/data/role.rb +449 -0
  72. data/lib/onyxcord/data/role_connection_data.rb +69 -0
  73. data/lib/onyxcord/data/role_subscription.rb +41 -0
  74. data/lib/onyxcord/data/scheduled_event.rb +513 -0
  75. data/lib/onyxcord/data/server.rb +1614 -0
  76. data/lib/onyxcord/data/server_preview.rb +68 -0
  77. data/lib/onyxcord/data/snapshot.rb +112 -0
  78. data/lib/onyxcord/data/team.rb +98 -0
  79. data/lib/onyxcord/data/timestamp.rb +69 -0
  80. data/lib/onyxcord/data/user.rb +324 -0
  81. data/lib/onyxcord/data/voice_region.rb +46 -0
  82. data/lib/onyxcord/data/voice_state.rb +41 -0
  83. data/lib/onyxcord/data/webhook.rb +238 -0
  84. data/lib/onyxcord/data.rb +57 -0
  85. data/lib/onyxcord/errors.rb +246 -0
  86. data/lib/onyxcord/event_executor.rb +80 -0
  87. data/lib/onyxcord/events/await.rb +48 -0
  88. data/lib/onyxcord/events/bans.rb +60 -0
  89. data/lib/onyxcord/events/channels.rb +225 -0
  90. data/lib/onyxcord/events/generic.rb +129 -0
  91. data/lib/onyxcord/events/guilds.rb +269 -0
  92. data/lib/onyxcord/events/integrations.rb +100 -0
  93. data/lib/onyxcord/events/interactions.rb +624 -0
  94. data/lib/onyxcord/events/invites.rb +127 -0
  95. data/lib/onyxcord/events/lifetime.rb +31 -0
  96. data/lib/onyxcord/events/members.rb +110 -0
  97. data/lib/onyxcord/events/message.rb +399 -0
  98. data/lib/onyxcord/events/polls.rb +118 -0
  99. data/lib/onyxcord/events/presence.rb +131 -0
  100. data/lib/onyxcord/events/raw.rb +74 -0
  101. data/lib/onyxcord/events/reactions.rb +218 -0
  102. data/lib/onyxcord/events/roles.rb +87 -0
  103. data/lib/onyxcord/events/scheduled_events.rb +171 -0
  104. data/lib/onyxcord/events/threads.rb +100 -0
  105. data/lib/onyxcord/events/typing.rb +73 -0
  106. data/lib/onyxcord/events/voice_server_update.rb +48 -0
  107. data/lib/onyxcord/events/voice_state_update.rb +106 -0
  108. data/lib/onyxcord/events/webhooks.rb +65 -0
  109. data/lib/onyxcord/gateway.rb +890 -0
  110. data/lib/onyxcord/id_object.rb +39 -0
  111. data/lib/onyxcord/light/data.rb +62 -0
  112. data/lib/onyxcord/light/integrations.rb +73 -0
  113. data/lib/onyxcord/light/light_bot.rb +58 -0
  114. data/lib/onyxcord/light.rb +8 -0
  115. data/lib/onyxcord/logger.rb +120 -0
  116. data/lib/onyxcord/message_components.rb +70 -0
  117. data/lib/onyxcord/paginator.rb +60 -0
  118. data/lib/onyxcord/permissions.rb +255 -0
  119. data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
  120. data/lib/onyxcord/rate_limiter/rest.rb +89 -0
  121. data/lib/onyxcord/version.rb +7 -0
  122. data/lib/onyxcord/voice/encoder.rb +115 -0
  123. data/lib/onyxcord/voice/network.rb +380 -0
  124. data/lib/onyxcord/voice/opcodes.rb +29 -0
  125. data/lib/onyxcord/voice/sodium.rb +157 -0
  126. data/lib/onyxcord/voice/timer.rb +19 -0
  127. data/lib/onyxcord/voice/voice_bot.rb +386 -0
  128. data/lib/onyxcord/webhooks.rb +14 -0
  129. data/lib/onyxcord/websocket.rb +62 -0
  130. data/lib/onyxcord.rb +180 -0
  131. data/onyxcord-webhooks.gemspec +30 -0
  132. data/onyxcord.gemspec +50 -0
  133. metadata +421 -0
@@ -0,0 +1,612 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/message_components'
4
+
5
+ module OnyxCord
6
+ # A message on Discord that was sent to a text channel
7
+ class Message
8
+ include IDObject
9
+
10
+ # Map of message flags.
11
+ FLAGS = {
12
+ crossposted: 1 << 0,
13
+ crosspost: 1 << 1,
14
+ suppress_embeds: 1 << 2,
15
+ source_message_deleted: 1 << 3,
16
+ urgent: 1 << 4,
17
+ thread: 1 << 5,
18
+ ephemeral: 1 << 6,
19
+ loading: 1 << 7,
20
+ failed_to_mention_roles: 1 << 8,
21
+ suppress_notifications: 1 << 12,
22
+ voice_message: 1 << 13,
23
+ snapshot: 1 << 14,
24
+ uikit_components: 1 << 15,
25
+ is_components_v2: OnyxCord::MessageComponents::IS_COMPONENTS_V2,
26
+ components_v2: OnyxCord::MessageComponents::IS_COMPONENTS_V2
27
+ }.freeze
28
+
29
+ # Map of message types.
30
+ TYPES = {
31
+ default: 0,
32
+ recipient_add: 1,
33
+ recipient_remove: 2,
34
+ call: 3,
35
+ channel_name_change: 4,
36
+ channel_icon_change: 5,
37
+ channel_pinned_message: 6,
38
+ server_member_join: 7,
39
+ server_boost: 8,
40
+ server_boost_tier_one: 9,
41
+ server_boost_tier_two: 10,
42
+ server_boost_tier_three: 11,
43
+ channel_follow_add: 12,
44
+ server_discovery_disqualified: 14,
45
+ server_discovery_requalified: 15,
46
+ server_discovery_grace_period_initial_warning: 16,
47
+ server_discovery_grace_period_final_warning: 17,
48
+ thread_created: 18,
49
+ reply: 19,
50
+ chat_input_command: 20,
51
+ thread_starter_message: 21,
52
+ server_invite_reminder: 22,
53
+ context_menu_command: 23,
54
+ automod_action: 24,
55
+ role_subscription_purchase: 25,
56
+ interaction_premium_upsell: 26,
57
+ stage_start: 27,
58
+ stage_end: 28,
59
+ stage_speaker: 29,
60
+ stage_raise_hand: 30,
61
+ stage_topic: 31,
62
+ server_application_premium_subscription: 32,
63
+ server_incident_alert_mode_enabled: 36,
64
+ server_incident_alert_mode_disabled: 37,
65
+ server_incident_report_raid: 38,
66
+ server_incident_report_false_alarm: 39,
67
+ purchase_notification: 44,
68
+ poll_result: 46,
69
+ changelog: 47,
70
+ server_join_request_accepted: 52,
71
+ server_join_request_rejected: 53,
72
+ server_join_request_withdrawn: 54,
73
+ report_to_mod_deleted_message: 58,
74
+ report_to_mod_timeout_user: 59,
75
+ report_to_mod_kick_user: 60,
76
+ report_to_mod_ban_user: 61,
77
+ report_to_mod_closed_report: 62,
78
+ server_emoji_added: 63
79
+ }.freeze
80
+
81
+ # @return [String] the content of this message.
82
+ attr_reader :content
83
+ alias_method :text, :content
84
+ alias_method :to_s, :content
85
+
86
+ # @return [Channel] the channel in which this message was sent.
87
+ attr_reader :channel
88
+
89
+ # @return [Time] the timestamp at which this message was edited. `nil` if the message was never edited.
90
+ attr_reader :edited_timestamp
91
+ alias_method :edit_timestamp, :edited_timestamp
92
+
93
+ # @return [Array<User>] the users that were mentioned in this message.
94
+ attr_reader :mentions
95
+
96
+ # @return [Array<Attachment>] the files attached to this message.
97
+ attr_reader :attachments
98
+
99
+ # @return [Array<Embed>] the embed objects contained in this message.
100
+ attr_reader :embeds
101
+
102
+ # @return [Array<Reaction>] the reaction objects contained in this message.
103
+ attr_reader :reactions
104
+
105
+ # @return [true, false] whether the message used Text-To-Speech (TTS) or not.
106
+ attr_reader :tts
107
+ alias_method :tts?, :tts
108
+
109
+ # @return [String] used for validating a message was sent.
110
+ attr_reader :nonce
111
+
112
+ # @return [true, false] whether the message was edited or not.
113
+ attr_reader :edited
114
+ alias_method :edited?, :edited
115
+
116
+ # @return [true, false] whether the message mentioned everyone or not.
117
+ attr_reader :mention_everyone
118
+ alias_method :mention_everyone?, :mention_everyone
119
+ alias_method :mentions_everyone?, :mention_everyone
120
+
121
+ # @return [true, false] whether the message is pinned or not.
122
+ attr_reader :pinned
123
+ alias_method :pinned?, :pinned
124
+
125
+ # @return [Integer] what the type of the message is
126
+ attr_reader :type
127
+
128
+ # @return [Integer, nil] the webhook ID that sent this message, or `nil` if it wasn't sent through a webhook.
129
+ attr_reader :webhook_id
130
+
131
+ # @return [Array<Component>] Interaction components for this message.
132
+ attr_reader :components
133
+
134
+ # @return [Integer] flags set on the message.
135
+ attr_reader :flags
136
+
137
+ # @return [Channel, nil] The thread that was started from this message, or nil.
138
+ attr_reader :thread
139
+
140
+ # @return [Time, nil] the time at when this message was pinned. Only present on messages fetched via {Channel#pins}.
141
+ attr_reader :pinned_at
142
+
143
+ # @return [Call, nil] the call in a private channel that prompted this message.
144
+ attr_reader :call
145
+
146
+ # @return [Array<Snapshot>] the message snapshots included in this message.
147
+ attr_reader :snapshots
148
+
149
+ # @return [RoleSubscriptionData, nil] the role subscription purchase or renewal that prompted this message.
150
+ attr_reader :role_subscription
151
+
152
+ # @return [Integer] a generally increasing integer that can be used to determine this message's position in a thread.
153
+ attr_reader :position
154
+
155
+ # @return [Poll, nil] the poll that was sent with this message, or `nil`.
156
+ attr_reader :poll
157
+
158
+ # @return [Poll::Result, nil] the finalised results for a poll that prompted this message.
159
+ attr_reader :poll_result
160
+ alias_method :poll_results, :poll_result
161
+
162
+ # @return [Integer, nil] the ID of the application associated with this message.
163
+ attr_reader :application_id
164
+
165
+ # @return [MessageActivity, nil] the rich-presence activity that prompted this message.
166
+ attr_reader :activity
167
+
168
+ # @return [Interactions::Metadata, nil] the metadata about the interaction that prompted this message.
169
+ attr_reader :interaction_metadata
170
+
171
+ # @!visibility private
172
+ def initialize(data, bot)
173
+ @bot = bot
174
+ @id = data['id'].to_i
175
+ @content = data['content']
176
+ @channel = bot.channel(data['channel_id'].to_i)
177
+ @pinned = data['pinned']
178
+ @type = data['type']
179
+ @tts = data['tts']
180
+ @nonce = data['nonce']
181
+ @flags = data['flags'] || 0
182
+ @position = data['position'] || 0
183
+ @mention_everyone = data['mention_everyone']
184
+ @webhook_id = data['webhook_id']&.to_i
185
+ @application_id = data['application_id']&.to_i
186
+
187
+ @edited_timestamp = Time.parse(data['edited_timestamp']) if data['edited_timestamp']
188
+ @edited = !@edited_timestamp.nil?
189
+
190
+ @message_reference = data['message_reference']
191
+ @referenced_message = Message.new(data['referenced_message'], @bot) if data['referenced_message']
192
+
193
+ if data['author']
194
+ if @webhook_id
195
+ # This is a webhook user! It would be pointless to try to resolve a member here, so we just create
196
+ # a User and return that instead.
197
+ OnyxCord::LOGGER.debug("Webhook user: #{data['author']['id']}")
198
+ @author = User.new(data['author'].merge({ '_webhook' => true }), @bot)
199
+ elsif @channel.private?
200
+
201
+ # Turn the message user into a recipient - we can't use the channel recipient
202
+ # directly because the bot may also send messages to the channel
203
+ @author = Recipient.new(@bot.user(data['author']['id'].to_i), @channel, @bot)
204
+ else
205
+ @author_id = data['author']['id'].to_i
206
+ end
207
+ end
208
+
209
+ @reactions = data['reactions']&.map { |reaction| Reaction.new(reaction) } || []
210
+ @mentions = data['mentions']&.map { |mention| @bot.ensure_user(mention) } || []
211
+ @mention_roles = data['mention_roles']&.map(&:to_i) || []
212
+ @poll_result = Poll::Result.new(data['embeds'].pop, @message_reference, @bot) if @type == 46
213
+
214
+ @attachments = data['attachments']&.map { |attachment| Attachment.new(attachment, self, @bot) } || []
215
+ @embeds = data['embeds']&.map { |embed| Embed.new(embed, self) } || []
216
+ @components = data['components']&.filter_map { |component| Components.from_data(component, @bot) } || []
217
+
218
+ @thread = @bot.ensure_channel(data['thread']) if data['thread']
219
+ @pinned_at = Time.parse(data['pinned_at']) if data['pinned_at']
220
+ @call = Call.new(data['call'], @bot) if data['call']
221
+ @poll = Poll.new(data['poll'], self, @bot) if data['poll']
222
+
223
+ @snapshots = data['message_snapshots']&.map { |snapshot| Snapshot.new(snapshot['message'], @bot) } || []
224
+ @role_subscription = RoleSubscriptionData.new(data['role_subscription_data'], self, @bot) if data['role_subscription_data']
225
+ @activity = MessageActivity.new(data['activity'], @bot) if data['activity']
226
+ @interaction_metadata = Interactions::Metadata.new(data['interaction_metadata'], self, @bot) if data['interaction_metadata']
227
+ end
228
+
229
+ # @deprecated Please migrate to using {#creation_time} instead.
230
+ alias_method :timestamp, :creation_time
231
+
232
+ # @return [Member, User] the user that sent this message. (Will be a {Member} most of the time, it should only be a
233
+ # {User} for old messages when the author has left the server since then)
234
+ def author
235
+ return @author if @author
236
+
237
+ unless @channel.private?
238
+ @author = @channel.server.member(@author_id)
239
+ OnyxCord::LOGGER.debug("Member with ID #{@author_id} not cached (possibly left the server).") if @author.nil?
240
+ end
241
+
242
+ @author ||= @bot.user(@author_id)
243
+ end
244
+
245
+ alias_method :user, :author
246
+ alias_method :writer, :author
247
+
248
+ # @return [Server, nil] the server this message was sent in. If this message was sent in a PM channel, it will be nil.
249
+ # @raise [OnyxCord::Errors::NoPermission] This can happen when receiving interactions for servers in which the bot is not
250
+ # authorized with the `bot` scope.
251
+ def server
252
+ return if @channel.private?
253
+
254
+ @server ||= @channel.server
255
+ end
256
+
257
+ # Get the roles that were mentioned in this message.
258
+ # @return [Array<Role>] the roles that were mentioned in this message.
259
+ # @raise [OnyxCord::Errors::NoPermission] This can happen when receiving interactions for servers in which the bot is not
260
+ # authorized with the `bot` scope.
261
+ def role_mentions
262
+ return [] if @channel.private? || @mention_roles.empty?
263
+
264
+ @role_mentions ||= @mention_roles.map { |id| server.role(id) }
265
+ end
266
+
267
+ # Replies to this message with the specified content.
268
+ # @deprecated Please use {#respond}.
269
+ # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
270
+ # @return (see #respond)
271
+ # @see Channel#send_message
272
+ def reply(content)
273
+ @channel.send_message(content)
274
+ end
275
+
276
+ # Responds to this message as an inline reply.
277
+ # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error.
278
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
279
+ # @param embed [Hash, OnyxCord::Webhooks::Embed, nil] The rich embed to append to this message.
280
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
281
+ # @param allowed_mentions [Hash, OnyxCord::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
282
+ # @param mention_user [true, false] Whether the user that is being replied to should be pinged by the reply.
283
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
284
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) and SUPPRESS_NOTIFICATIONS (1 << 12) can be set.
285
+ # @return (see #respond)
286
+ def reply!(content, tts: false, embed: nil, attachments: nil, allowed_mentions: {}, mention_user: false, components: nil, flags: 0)
287
+ allowed_mentions = { parse: [] } if allowed_mentions == false
288
+ allowed_mentions = allowed_mentions.to_hash.transform_keys(&:to_sym)
289
+ allowed_mentions[:replied_user] = mention_user
290
+
291
+ respond(content, tts, embed, attachments, allowed_mentions, self, components, flags)
292
+ end
293
+
294
+ # (see Channel#send_message)
295
+ def respond(content, tts = false, embed = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0)
296
+ @channel.send_message(content, tts, embed, attachments, allowed_mentions, message_reference, components, flags)
297
+ end
298
+
299
+ # Edits this message to have the specified content instead.
300
+ # You can only edit your own messages.
301
+ # @param new_content [String] the new content the message should have.
302
+ # @param new_embeds [Hash, OnyxCord::Webhooks::Embed, Array<Hash>, Array<OnyxCord::Webhooks::Embed>, nil] The new embeds the message should have. If `nil` the message will be changed to have no embeds.
303
+ # @param new_components [View, Array<Hash>] The new components the message should have. If `nil` the message will be changed to have no components.
304
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) can be edited.
305
+ # @return [Message] the resulting message.
306
+ def edit(new_content, new_embeds = nil, new_components = nil, flags = 0)
307
+ new_embeds = (new_embeds.instance_of?(Array) ? new_embeds.map(&:to_hash) : [new_embeds&.to_hash]).compact
308
+ new_components = new_components.to_a
309
+ flags = OnyxCord::MessageComponents.apply_v2_flag(flags, new_components)
310
+
311
+ response = API::Channel.edit_message(@bot.token, @channel.id, @id, new_content, :undef, new_embeds, new_components, flags)
312
+ Message.new(JSON.parse(response), @bot)
313
+ end
314
+
315
+ # Deletes this message.
316
+ # @return [nil]
317
+ def delete(reason = nil)
318
+ API::Channel.delete_message(@bot.token, @channel.id, @id, reason)
319
+ nil
320
+ end
321
+
322
+ # Pins this message
323
+ # @return [nil]
324
+ def pin(reason = nil)
325
+ API::Channel.pin_message(@bot.token, @channel.id, @id, reason)
326
+ @pinned = true
327
+ nil
328
+ end
329
+
330
+ # Unpins this message
331
+ # @return [nil]
332
+ def unpin(reason = nil)
333
+ API::Channel.unpin_message(@bot.token, @channel.id, @id, reason)
334
+ @pinned = false
335
+ nil
336
+ end
337
+
338
+ # Crossposts a message in a news channel.
339
+ # @return [Message] the updated message object.
340
+ def crosspost
341
+ response = API::Channel.crosspost_message(@bot.token, @channel.id, @id)
342
+ Message.new(JSON.parse(response), @bot)
343
+ end
344
+
345
+ # Add an {Await} for a message with the same user and channel.
346
+ # @see Bot#add_await
347
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#await!} instead.
348
+ def await(key, attributes = {}, &block)
349
+ @bot.add_await(key, OnyxCord::Events::MessageEvent, { from: @author.id, in: @channel.id }.merge(attributes), &block)
350
+ end
351
+
352
+ # Add a blocking {Await} for a message with the same user and channel.
353
+ # @see Bot#add_await!
354
+ def await!(attributes = {}, &block)
355
+ @bot.add_await!(OnyxCord::Events::MessageEvent, { from: @author.id, in: @channel.id }.merge(attributes), &block)
356
+ end
357
+
358
+ # Add an {Await} for a reaction to be added on this message.
359
+ # @see Bot#add_await
360
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#await_reaction!} instead.
361
+ def await_reaction(key, attributes = {}, &block)
362
+ @bot.add_await(key, OnyxCord::Events::ReactionAddEvent, { message: @id }.merge(attributes), &block)
363
+ end
364
+
365
+ # Add a blocking {Await} for a reaction to be added on this message.
366
+ # @see Bot#add_await!
367
+ def await_reaction!(attributes = {}, &block)
368
+ @bot.add_await!(OnyxCord::Events::ReactionAddEvent, { message: @id }.merge(attributes), &block)
369
+ end
370
+
371
+ # @return [true, false] whether this message was sent by the current {Bot}.
372
+ def from_bot?
373
+ (@author_id || @author&.id) == @bot.profile.id
374
+ end
375
+
376
+ # @return [true, false] whether this message has been sent over a webhook.
377
+ def webhook?
378
+ !@webhook_id.nil?
379
+ end
380
+
381
+ # @return [Array<Emoji>] the emotes that were used/mentioned in this message.
382
+ def emoji
383
+ return [] if @content.empty? || @content.nil?
384
+
385
+ @emoji ||= @bot.parse_mentions(@content).select { |mention| mention.is_a?(OnyxCord::Emoji) }
386
+ end
387
+
388
+ alias_method :emojis, :emoji
389
+
390
+ # Check if any emoji were used in this message.
391
+ # @return [true, false] whether or not any emoji were used
392
+ def emoji?
393
+ emoji.any?
394
+ end
395
+
396
+ alias_method :emojis?, :emoji?
397
+
398
+ # Check if any reactions were used in this message.
399
+ # @return [true, false] whether or not this message has reactions
400
+ def reactions?
401
+ !@reactions.empty?
402
+ end
403
+
404
+ # Returns the reactions made by the current bot or user.
405
+ # @return [Array<Reaction>] the reactions
406
+ def my_reactions
407
+ @reactions.select(&:me)
408
+ end
409
+
410
+ # Removes embeds from the message
411
+ # @return [Message] the resulting message.
412
+ def suppress_embeds
413
+ flags = @flags | (1 << 2)
414
+ response = API::Channel.edit_message(@bot.token, @channel.id, @id, :undef, :undef, :undef, :undef, flags)
415
+ Message.new(JSON.parse(response), @bot)
416
+ end
417
+
418
+ # Check if this message mentions a specific user or role.
419
+ # @param target [Role, User, Member, Integer, String] The mention to match against.
420
+ # @return [true, false] whether or not this message mentions the target.
421
+ def mentions?(target)
422
+ mentions = (@mentions + role_mentions)
423
+
424
+ mentions << server if @mention_everyone
425
+
426
+ mentions.any?(target.resolve_id)
427
+ end
428
+
429
+ # Reacts to a message.
430
+ # @param reaction [String, #to_reaction] the unicode emoji or {Emoji}
431
+ # @return [nil]
432
+ def create_reaction(reaction)
433
+ reaction = reaction.to_reaction if reaction.respond_to?(:to_reaction)
434
+ API::Channel.create_reaction(@bot.token, @channel.id, @id, reaction)
435
+ nil
436
+ end
437
+
438
+ alias_method :react, :create_reaction
439
+
440
+ # Returns the list of users who reacted with a certain reaction.
441
+ # @param reaction [String, #to_reaction] the unicode emoji or {Emoji}
442
+ # @param limit [Integer] the limit of how many users to retrieve. `nil` will return all users
443
+ # @param type [Integer, Symbol] the type of reaction to get. See {Reaction::TYPES}
444
+ # @example Get all the users that reacted with a thumbs up.
445
+ # thumbs_up_reactions = message.reacted_with("\u{1F44D}")
446
+ # @return [Array<User>] the users who used this reaction
447
+ def reacted_with(reaction, limit: 100, type: :normal)
448
+ reaction = reaction.to_reaction if reaction.respond_to?(:to_reaction)
449
+ reaction = reaction.to_s if reaction.respond_to?(:to_s)
450
+ type = Reaction::TYPES[type] || type
451
+
452
+ get_reactions = proc do |fetch_limit, after_id = nil|
453
+ resp = API::Channel.get_reactions(@bot.token, @channel.id, @id, reaction, nil, after_id, fetch_limit, type)
454
+ JSON.parse(resp).map { |d| User.new(d, @bot) }
455
+ end
456
+
457
+ # Can be done without pagination
458
+ return get_reactions.call(limit) if limit && limit <= 100
459
+
460
+ paginator = Paginator.new(limit, :down) do |last_page|
461
+ if last_page && last_page.count < 100
462
+ []
463
+ else
464
+ get_reactions.call(100, last_page&.last&.id)
465
+ end
466
+ end
467
+
468
+ paginator.to_a
469
+ end
470
+
471
+ # Returns a hash of all reactions to a message as keys and the users that reacted to it as values.
472
+ # @param limit [Integer] the limit of how many users to retrieve per distinct reaction emoji. `nil` will return all users
473
+ # @example Get all the users that reacted to a message for a giveaway.
474
+ # giveaway_participants = message.all_reaction_users
475
+ # @return [Hash<String => Array<User>>] A hash mapping the string representation of a
476
+ # reaction to an array of users.
477
+ def all_reaction_users(limit: 100)
478
+ @reactions.to_h { |reaction| [reaction.to_s, reacted_with(reaction, limit: limit)] }
479
+ end
480
+
481
+ # Deletes a reaction made by a user on this message.
482
+ # @param user [User, String, Integer] the user or user ID who used this reaction
483
+ # @param reaction [String, #to_reaction] the reaction to remove
484
+ def delete_reaction(user, reaction)
485
+ reaction = reaction.to_reaction if reaction.respond_to?(:to_reaction)
486
+ API::Channel.delete_user_reaction(@bot.token, @channel.id, @id, reaction, user.resolve_id)
487
+ end
488
+
489
+ # Deletes this client's reaction on this message.
490
+ # @param reaction [String, #to_reaction] the reaction to remove
491
+ def delete_own_reaction(reaction)
492
+ reaction = reaction.to_reaction if reaction.respond_to?(:to_reaction)
493
+ API::Channel.delete_own_reaction(@bot.token, @channel.id, @id, reaction)
494
+ end
495
+
496
+ # Removes all reactions from this message.
497
+ def delete_all_reactions
498
+ API::Channel.delete_all_reactions(@bot.token, @channel.id, @id)
499
+ end
500
+
501
+ # Removes all reactions for a single emoji.
502
+ # @param reaction [String, #to_reaction] the reaction to remove.
503
+ def delete_all_reactions_for_emoji(reaction)
504
+ reaction = reaction.to_reaction if reaction.respond_to?(:to_reaction)
505
+ API::Channel.delete_all_emoji_reactions(@bot.token, @channel.id, @id, reaction.to_s)
506
+ end
507
+
508
+ # The inspect method is overwritten to give more useful output
509
+ def inspect
510
+ "<Message content=\"#{@content}\" id=#{@id} timestamp=#{@timestamp} author=#{@author} channel=#{@channel}>"
511
+ end
512
+
513
+ # @return [String] a URL that a user can use to navigate to this message in the client
514
+ def link
515
+ "https://discord.com/channels/#{server&.id || '@me'}/#{@channel.id}/#{@id}"
516
+ end
517
+
518
+ alias_method :jump_link, :link
519
+
520
+ # Whether or not this message was sent in reply to another message
521
+ # @return [true, false]
522
+ def reply?
523
+ !@referenced_message.nil?
524
+ end
525
+
526
+ # @return [Message, nil] the Message this Message was sent in reply to.
527
+ def referenced_message
528
+ return @referenced_message if @referenced_message
529
+ return nil unless @message_reference
530
+
531
+ referenced_channel = @bot.channel(@message_reference['channel_id'])
532
+ @referenced_message = referenced_channel&.message(@message_reference['message_id'])
533
+ end
534
+
535
+ # @return [Array<Components::Button>]
536
+ def buttons
537
+ buttons = @components.flat_map do |component|
538
+ case component
539
+ when Components::Button
540
+ component
541
+ when Components::Section
542
+ component.accessory if component.accessory.is_a?(Components::Button)
543
+ when Components::ActionRow, Components::Container
544
+ component.buttons
545
+ end
546
+ end
547
+
548
+ buttons.compact
549
+ end
550
+
551
+ # to_message -> self or message
552
+ # @return [OnyxCord::Message]
553
+ def to_message
554
+ self
555
+ end
556
+
557
+ alias_method :message, :to_message
558
+
559
+ FLAGS.each do |name, value|
560
+ define_method("#{name}?") do
561
+ @flags.anybits?(value)
562
+ end
563
+ end
564
+
565
+ TYPES.each do |name, value|
566
+ define_method("#{name}?") do
567
+ @type == value
568
+ end
569
+ end
570
+
571
+ # Convert this message to a hash that can be used to reference this message in a forward or a reply.
572
+ # @param type [Integer, Symbol] The reference type to set. Can either be one of `:reply` or `:forward`.
573
+ # @param must_exist [true, false] Whether to raise an error if this message was deleted when sending it.
574
+ # @return [Hash] the message as a hash representation that can be used in a forwarded message or a reply.
575
+ def to_reference(type: :reply, must_exist: true)
576
+ type = (type == :reply ? 0 : 1) if type.is_a?(Symbol)
577
+
578
+ { type: type, message_id: @id, channel_id: @channel.id, fail_if_not_exists: must_exist }
579
+ end
580
+
581
+ # Forward this message to another channel.
582
+ # @param channel [Integer, String, Channel] The target channel to forward this message to.
583
+ # @param must_exist [true, false] Whether to raise an error if this message was deleted when sending it.
584
+ # @param timeout [Float, nil] The amount of time in seconds after which the message sent will be deleted.
585
+ # @param flags [Integer, Symbol, Array<Integer, Symbol>] The message flags to set on the forwarded message.
586
+ # @param nonce [String, Integer, nil] The 25 character optional nonce that should be used when forwarding this message.
587
+ # @param enforce_nonce [true, false] Whether the provided nonce should be enforced and used for message de-duplication.
588
+ # @return [Message, nil] the message that was created from forwarding this one, or `nil` if this is a temporary message.
589
+ def forward(channel, must_exist: true, timeout: nil, flags: 0, nonce: nil, enforce_nonce: false)
590
+ reference = to_reference(type: :forward, must_exist: must_exist)
591
+
592
+ @bot.channel(channel).send_message!(reference: reference, timeout: timeout, flags: flags, nonce: nonce, enforce_nonce: enforce_nonce)
593
+ end
594
+
595
+ # Get the formatted timestamps contained in the message content.
596
+ # @return [Array<TimestampMarkdown>] The formatted timestamps in the message.
597
+ def timestamps
598
+ return (@timestamps || []) if @timestamps || !@content || @content.empty?
599
+
600
+ @timestamps = []
601
+
602
+ @content.scan(/<t:(-?\d{1,13})(?::(t|T|d|D|f|F|s|S|R))?>/) do |time, style|
603
+ # If it's not between these values, Discord won't show it, so don't bother.
604
+ if (time = time.to_i).between?(-8_640_000_000_000, 8_640_000_000_000)
605
+ @timestamps << TimestampMarkdown.new(Time.at(time), style)
606
+ end
607
+ end
608
+
609
+ @timestamps
610
+ end
611
+ end
612
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ # A rich-presence activity attached to a message.
5
+ class MessageActivity
6
+ # Map of activity types.
7
+ TYPES = {
8
+ join: 1,
9
+ spectate: 2,
10
+ listen: 3,
11
+ join_request: 5
12
+ }.freeze
13
+
14
+ # @return [Integer] the type of the activity.
15
+ attr_reader :type
16
+
17
+ # @return [String, nil] the party ID of the activity.
18
+ attr_reader :party_id
19
+
20
+ # @!visibility private
21
+ def initialize(data, bot)
22
+ @bot = bot
23
+ @type = data['type']
24
+ @party_id = data['party_id']
25
+ end
26
+
27
+ # @!method join?
28
+ # @return [true, false] whether or not the activity type is join.
29
+ # @!method spectate?
30
+ # @return [true, false] whether or not the activity type is spectate.
31
+ # @!method listen?
32
+ # @return [true, false] whether or not the activity type is listen.
33
+ # @!method join_request?
34
+ # @return [true, false] whether or not the activity type is a join request.
35
+ TYPES.each do |name, value|
36
+ define_method("#{name}?") do
37
+ @type == value
38
+ end
39
+ end
40
+ end
41
+ end