discord_rda 0.1.3

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +398 -0
  4. data/lib/discord_rda/bot.rb +842 -0
  5. data/lib/discord_rda/cache/configurable_cache.rb +283 -0
  6. data/lib/discord_rda/cache/entity_cache.rb +184 -0
  7. data/lib/discord_rda/cache/memory_store.rb +143 -0
  8. data/lib/discord_rda/cache/redis_store.rb +136 -0
  9. data/lib/discord_rda/cache/store.rb +56 -0
  10. data/lib/discord_rda/connection/gateway_client.rb +383 -0
  11. data/lib/discord_rda/connection/invalid_bucket.rb +205 -0
  12. data/lib/discord_rda/connection/rate_limiter.rb +280 -0
  13. data/lib/discord_rda/connection/request_queue.rb +340 -0
  14. data/lib/discord_rda/connection/reshard_manager.rb +328 -0
  15. data/lib/discord_rda/connection/rest_client.rb +316 -0
  16. data/lib/discord_rda/connection/rest_proxy.rb +165 -0
  17. data/lib/discord_rda/connection/scalable_rest_client.rb +526 -0
  18. data/lib/discord_rda/connection/shard_manager.rb +223 -0
  19. data/lib/discord_rda/core/async_runtime.rb +108 -0
  20. data/lib/discord_rda/core/configuration.rb +194 -0
  21. data/lib/discord_rda/core/logger.rb +188 -0
  22. data/lib/discord_rda/core/snowflake.rb +121 -0
  23. data/lib/discord_rda/entity/attachment.rb +88 -0
  24. data/lib/discord_rda/entity/base.rb +103 -0
  25. data/lib/discord_rda/entity/channel.rb +446 -0
  26. data/lib/discord_rda/entity/channel_builder.rb +280 -0
  27. data/lib/discord_rda/entity/color.rb +253 -0
  28. data/lib/discord_rda/entity/embed.rb +221 -0
  29. data/lib/discord_rda/entity/emoji.rb +89 -0
  30. data/lib/discord_rda/entity/factory.rb +99 -0
  31. data/lib/discord_rda/entity/guild.rb +619 -0
  32. data/lib/discord_rda/entity/member.rb +263 -0
  33. data/lib/discord_rda/entity/message.rb +405 -0
  34. data/lib/discord_rda/entity/message_builder.rb +369 -0
  35. data/lib/discord_rda/entity/role.rb +157 -0
  36. data/lib/discord_rda/entity/support.rb +294 -0
  37. data/lib/discord_rda/entity/user.rb +231 -0
  38. data/lib/discord_rda/entity/value_objects.rb +263 -0
  39. data/lib/discord_rda/event/auto_moderation.rb +294 -0
  40. data/lib/discord_rda/event/base.rb +986 -0
  41. data/lib/discord_rda/event/bus.rb +225 -0
  42. data/lib/discord_rda/event/scheduled_event.rb +257 -0
  43. data/lib/discord_rda/hot_reload_manager.rb +303 -0
  44. data/lib/discord_rda/interactions/application_command.rb +436 -0
  45. data/lib/discord_rda/interactions/command_system.rb +484 -0
  46. data/lib/discord_rda/interactions/components.rb +464 -0
  47. data/lib/discord_rda/interactions/interaction.rb +553 -0
  48. data/lib/discord_rda/plugin/analytics_plugin.rb +528 -0
  49. data/lib/discord_rda/plugin/base.rb +190 -0
  50. data/lib/discord_rda/plugin/registry.rb +126 -0
  51. data/lib/discord_rda/version.rb +5 -0
  52. data/lib/discord_rda.rb +70 -0
  53. metadata +302 -0
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscordRDA
4
+ # Represents a Discord guild member.
5
+ # Combines User data with guild-specific information.
6
+ #
7
+ class Member < Entity
8
+ # Member inherits ID from user
9
+ def id
10
+ user&.id
11
+ end
12
+
13
+ # @return [User] The underlying user
14
+ attr_reader :user
15
+
16
+ # @return [String] Nickname in the guild
17
+ attr_reader :nick
18
+
19
+ # @return [String] Guild avatar hash
20
+ attr_reader :avatar
21
+
22
+ # @return [Array<Snowflake>] Role IDs
23
+ attr_reader :roles
24
+
25
+ # @return [Time] When member joined
26
+ attr_reader :joined_at
27
+
28
+ # @return [Time] When member started boosting
29
+ attr_reader :premium_since
30
+
31
+ # @return [Boolean] Whether member is deafened
32
+ attr_reader :deaf
33
+
34
+ # @return [Boolean] Whether member is muted
35
+ attr_reader :mute
36
+
37
+ # @return [Integer] Guild flags
38
+ attr_reader :flags
39
+
40
+ # @return [Boolean] Whether member is pending
41
+ attr_reader :pending
42
+
43
+ # @return [String] Permissions for this member in channel
44
+ attr_reader :permissions
45
+
46
+ # @return [Time] When member's timeout expires
47
+ attr_reader :communication_disabled_until
48
+
49
+ # Create a new member
50
+ # @param data [Hash] Member data
51
+ def initialize(data = {})
52
+ super
53
+
54
+ @user = data['user'] ? User.new(data['user']) : nil
55
+ @nick = data['nick']
56
+ @avatar = data['avatar']
57
+ @roles = (data['roles'] || []).map { |r| Snowflake.new(r) }
58
+ @joined_at = data['joined_at'] ? Time.parse(data['joined_at']) : nil
59
+ @premium_since = data['premium_since'] ? Time.parse(data['premium_since']) : nil
60
+ @deaf = data['deaf'] || false
61
+ @mute = data['mute'] || false
62
+ @flags = data['flags'] || 0
63
+ @pending = data['pending'] || false
64
+ @permissions = data['permissions']
65
+ @communication_disabled_until = data['communication_disabled_until'] ? Time.parse(data['communication_disabled_until']) : nil
66
+ end
67
+
68
+ # Get the user object
69
+ # @return [User] User object
70
+ def user
71
+ @user
72
+ end
73
+
74
+ # Get member's effective name (nickname or username)
75
+ # @return [String] Display name
76
+ def display_name
77
+ nick || user&.display_name || user&.username
78
+ end
79
+
80
+ # Get mention string
81
+ # @return [String] Member mention
82
+ def mention
83
+ "<@!#{id}>"
84
+ end
85
+
86
+ # Check if member has a nickname
87
+ # @return [Boolean] True if has nickname
88
+ def nick?
89
+ !nick.nil?
90
+ end
91
+
92
+ # Get member's guild avatar URL
93
+ # @param format [String] Image format
94
+ # @param size [Integer] Image size
95
+ # @return [String] Avatar URL
96
+ def avatar_url(format: 'png', size: nil)
97
+ if avatar
98
+ url = "https://cdn.discordapp.com/guilds/#{guild_id}/users/#{id}/avatars/#{avatar}.#{format}"
99
+ url += "?size=#{size}" if size
100
+ url
101
+ else
102
+ user&.avatar_url(format: format, size: size)
103
+ end
104
+ end
105
+
106
+ # Check if member is server deafened
107
+ # @return [Boolean] True if deafened
108
+ def deaf?
109
+ deaf
110
+ end
111
+
112
+ # Check if member is server muted
113
+ # @return [Boolean] True if muted
114
+ def mute?
115
+ mute
116
+ end
117
+
118
+ # Check if member has pending membership screening
119
+ # @return [Boolean] True if pending
120
+ def pending?
121
+ pending
122
+ end
123
+
124
+ # Check if member is currently timed out
125
+ # @return [Boolean] True if timed out
126
+ def timed_out?
127
+ return false unless communication_disabled_until
128
+
129
+ communication_disabled_until > Time.now.utc
130
+ end
131
+
132
+ # Check if member is boosting
133
+ # @return [Boolean] True if boosting
134
+ def boosting?
135
+ !premium_since.nil?
136
+ end
137
+
138
+ # Get boost start time
139
+ # @return [Time, nil] When boosting started
140
+ def boost_since
141
+ premium_since
142
+ end
143
+
144
+ # Get how long member has been in the guild
145
+ # @return [Float] Duration in seconds
146
+ def duration_in_guild
147
+ return 0 unless joined_at
148
+
149
+ Time.now.utc - joined_at
150
+ end
151
+
152
+ # Get how long member has been boosting
153
+ # @return [Float, nil] Duration in seconds or nil if not boosting
154
+ def boost_duration
155
+ return nil unless premium_since
156
+
157
+ Time.now.utc - premium_since
158
+ end
159
+
160
+ # Get guild ID
161
+ # @return [Snowflake, nil] Guild ID
162
+ def guild_id
163
+ @raw_data['guild_id'] ? Snowflake.new(@raw_data['guild_id']) : nil
164
+ end
165
+
166
+ # Get permission set for this member
167
+ # @return [Permission, nil] Permissions
168
+ def permission_set
169
+ return nil unless permissions
170
+
171
+ Permission.new(permissions.to_i)
172
+ end
173
+
174
+ # Check if member has a specific role
175
+ # @param role_id [String, Snowflake] Role ID to check
176
+ # @return [Boolean] True if has role
177
+ def has_role?(role_id)
178
+ role_snowflake = role_id.is_a?(Snowflake) ? role_id : Snowflake.new(role_id)
179
+ roles.include?(role_snowflake)
180
+ end
181
+
182
+ # Get the highest role position
183
+ # @param guild_roles [Array<Role>] All guild roles
184
+ # @return [Integer] Highest position
185
+ def highest_role_position(guild_roles)
186
+ member_roles = guild_roles.select { |r| has_role?(r.id) }
187
+ member_roles.map(&:position).max || 0
188
+ end
189
+
190
+ # Check if member can perform action on target
191
+ # Compares roles and permissions
192
+ # @param target [Member] Target member
193
+ # @param guild_roles [Array<Role>] Guild roles for comparison
194
+ # @return [Boolean] True if this member outranks target
195
+ def can_act_on?(target, guild_roles)
196
+ return false if target.id == id # Can't act on self
197
+ return false if target.id == guild_id # Can't act on owner
198
+
199
+ highest_role_position(guild_roles) > target.highest_role_position(guild_roles)
200
+ end
201
+
202
+ # Get creation time (from user)
203
+ # @return [Time, nil] Account creation time
204
+ def created_at
205
+ user&.created_at
206
+ end
207
+
208
+ # Delegate methods to user
209
+ def method_missing(method, *args, &block)
210
+ if user&.respond_to?(method)
211
+ user.public_send(method, *args, &block)
212
+ else
213
+ super
214
+ end
215
+ end
216
+
217
+ def respond_to_missing?(method, include_private = false)
218
+ user&.respond_to?(method) || super
219
+ end
220
+
221
+ # Check if this member is the guild owner
222
+ # @param guild_owner_id [Snowflake] Guild owner ID
223
+ # @return [Boolean] True if owner
224
+ def owner?(guild_owner_id)
225
+ id == guild_owner_id
226
+ end
227
+
228
+ # Get member flags
229
+ # @return [MemberFlags] Member flags
230
+ def member_flags
231
+ MemberFlags.new(flags)
232
+ end
233
+
234
+ # Check if member has completed onboarding
235
+ # @return [Boolean] True if completed
236
+ def completed_onboarding?
237
+ !flags.nil? && (flags & 2) == 2
238
+ end
239
+
240
+ # Check if member has bypassed verification
241
+ # @return [Boolean] True if bypassed
242
+ def bypasses_verification?
243
+ !flags.nil? && (flags & 4) == 4
244
+ end
245
+
246
+ # Check if member started onboarding
247
+ # @return [Boolean] True if started
248
+ def started_onboarding?
249
+ !flags.nil? && (flags & 1) == 1
250
+ end
251
+
252
+ # Get member's display color from highest colored role
253
+ # @param guild_roles [Array<Role>] All guild roles
254
+ # @return [Color] Display color
255
+ def display_color(guild_roles)
256
+ member_roles = guild_roles.select { |r| has_role?(r.id) && r.color > 0 }
257
+ return Color.new(0) if member_roles.empty?
258
+
259
+ highest_colored = member_roles.max_by(&:position)
260
+ Color.new(highest_colored.color)
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,405 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require_relative 'message_builder'
5
+
6
+ module DiscordRDA
7
+ # Represents a Discord message.
8
+ # Messages can contain text, embeds, attachments, and more.
9
+ #
10
+ class Message < Entity
11
+ # Class-level API client for making REST requests
12
+ # @return [RestClient, ScalableRestClient, nil] The REST client to use
13
+ class << self
14
+ attr_accessor :api
15
+ end
16
+
17
+ # Message types
18
+ TYPES = {
19
+ default: 0,
20
+ recipient_add: 1,
21
+ recipient_remove: 2,
22
+ call: 3,
23
+ channel_name_change: 4,
24
+ channel_icon_change: 5,
25
+ channel_pinned_message: 6,
26
+ user_join: 7,
27
+ guild_boost: 8,
28
+ guild_boost_tier_1: 9,
29
+ guild_boost_tier_2: 10,
30
+ guild_boost_tier_3: 11,
31
+ channel_follow_add: 12,
32
+ guild_discovery_disqualified: 14,
33
+ guild_discovery_requalified: 15,
34
+ guild_discovery_grace_period_initial_warning: 16,
35
+ guild_discovery_grace_period_final_warning: 17,
36
+ thread_created: 18,
37
+ reply: 19,
38
+ chat_input_command: 20,
39
+ thread_starter_message: 21,
40
+ guild_invite_reminder: 22,
41
+ context_menu_command: 23,
42
+ auto_moderation_action: 24,
43
+ role_subscription_purchase: 25,
44
+ interaction_premium_upsell: 26,
45
+ stage_start: 27,
46
+ stage_end: 28,
47
+ stage_speaker: 29,
48
+ stage_topic: 31,
49
+ guild_application_premium_subscription: 32,
50
+ guild_incident_alert_mode_enabled: 36,
51
+ guild_incident_alert_mode_disabled: 37,
52
+ guild_incident_report_raid: 38,
53
+ guild_incident_report_false_alarm: 39,
54
+ purchase_notification: 44,
55
+ poll_result: 46
56
+ }.freeze
57
+
58
+ attribute :channel_id, type: :snowflake
59
+ attribute :author, type: :hash
60
+ attribute :content, type: :string, default: ''
61
+ attribute :timestamp, type: :time
62
+ attribute :edited_timestamp, type: :time
63
+ attribute :tts, type: :boolean, default: false
64
+ attribute :mention_everyone, type: :boolean, default: false
65
+ attribute :mentions, type: :array, default: []
66
+ attribute :mention_roles, type: :array, default: []
67
+ attribute :mention_channels, type: :array, default: []
68
+ attribute :attachments, type: :array, default: []
69
+ attribute :embeds, type: :array, default: []
70
+ attribute :reactions, type: :array, default: []
71
+ attribute :nonce, type: :string
72
+ attribute :pinned, type: :boolean, default: false
73
+ attribute :webhook_id, type: :snowflake
74
+ attribute :type, type: :integer, default: 0
75
+ attribute :activity, type: :hash
76
+ attribute :application, type: :hash
77
+ attribute :application_id, type: :snowflake
78
+ attribute :message_reference, type: :hash
79
+ attribute :flags, type: :integer, default: 0
80
+ attribute :referenced_message, type: :hash
81
+ attribute :interaction_metadata, type: :hash
82
+ attribute :interaction, type: :hash
83
+ attribute :api, type: :hash
84
+ attribute :components, type: :array, default: []
85
+ attribute :sticker_items, type: :array, default: []
86
+ attribute :stickers, type: :array, default: []
87
+ attribute :position, type: :integer
88
+ attribute :role_subscription_data, type: :hash
89
+ attribute :resolved, type: :hash
90
+ attribute :poll, type: :hash
91
+ attribute :call, type: :hash
92
+
93
+ # Get message type as symbol
94
+ # @return [Symbol] Message type
95
+ def message_type
96
+ TYPES.key(type) || :unknown
97
+ end
98
+
99
+ # Get the author as a User entity
100
+ # @return [User, Member] Author
101
+ def author
102
+ return nil unless @raw_data['author']
103
+
104
+ if @raw_data['member']
105
+ Member.new(@raw_data['author'].merge('member' => @raw_data['member'], 'guild_id' => @raw_data['guild_id']))
106
+ else
107
+ User.new(@raw_data['author'])
108
+ end
109
+ end
110
+
111
+ # Get mentioned users
112
+ # @return [Array<User>] Mentioned users
113
+ def mentioned_users
114
+ (@raw_data['mentions'] || []).map { |m| User.new(m) }
115
+ end
116
+
117
+ # Get mentioned roles as snowflakes
118
+ # @return [Array<Snowflake>] Mentioned role IDs
119
+ def mentioned_roles
120
+ (@raw_data['mention_roles'] || []).map { |r| Snowflake.new(r) }
121
+ end
122
+
123
+ # Get mentioned channels
124
+ # @return [Array<Channel>] Mentioned channels
125
+ def mentioned_channels
126
+ (@raw_data['mention_channels'] || []).map { |c| Channel.new(c) }
127
+ end
128
+
129
+ # Get attachments
130
+ # @return [Array<Attachment>] Attachments
131
+ def attachments
132
+ (@raw_data['attachments'] || []).map { |a| Attachment.new(a) }
133
+ end
134
+
135
+ # Get embeds
136
+ # @return [Array<Embed>] Embeds
137
+ def embeds
138
+ (@raw_data['embeds'] || []).map { |e| Embed.new(e) }
139
+ end
140
+
141
+ # Get reactions
142
+ # @return [Array<Reaction>] Reactions
143
+ def reactions
144
+ (@raw_data['reactions'] || []).map { |r| Reaction.new(r) }
145
+ end
146
+
147
+ # Check if message is TTS
148
+ # @return [Boolean] True if TTS
149
+ def tts?
150
+ tts
151
+ end
152
+
153
+ # Check if message mentions everyone
154
+ # @return [Boolean] True if mentions everyone
155
+ def mention_everyone?
156
+ mention_everyone
157
+ end
158
+
159
+ # Check if message is pinned
160
+ # @return [Boolean] True if pinned
161
+ def pinned?
162
+ pinned
163
+ end
164
+
165
+ # Check if message was edited
166
+ # @return [Boolean] True if edited
167
+ def edited?
168
+ !edited_timestamp.nil?
169
+ end
170
+
171
+ # Get edit timestamp
172
+ # @return [Time, nil] Edit time
173
+ def edited_at
174
+ edited_timestamp
175
+ end
176
+
177
+ # Check if this is a reply to another message
178
+ # @return [Boolean] True if reply
179
+ def reply?
180
+ type == 19 || !message_reference.nil?
181
+ end
182
+
183
+ # Get the referenced (replied to) message
184
+ # @return [Message, nil] Referenced message
185
+ def referenced_message
186
+ ref = @raw_data['referenced_message']
187
+ Message.new(ref) if ref
188
+ end
189
+
190
+ # Get the message reference data
191
+ # @return [Hash, nil] Message reference
192
+ def message_reference
193
+ @raw_data['message_reference']
194
+ end
195
+
196
+ # Get the jump URL for this message
197
+ # @return [String] Jump URL
198
+ def jump_url
199
+ guild_id = @raw_data['guild_id']
200
+ if guild_id
201
+ "https://discord.com/channels/#{guild_id}/#{channel_id}/#{id}"
202
+ else
203
+ "https://discord.com/channels/@me/#{channel_id}/#{id}"
204
+ end
205
+ end
206
+
207
+ # Check if message is from a webhook
208
+ # @return [Boolean] True if webhook message
209
+ def webhook?
210
+ !webhook_id.nil?
211
+ end
212
+
213
+ # Check if message has embeds
214
+ # @return [Boolean] True if has embeds
215
+ def has_embeds?
216
+ embeds.any?
217
+ end
218
+
219
+ # Check if message has attachments
220
+ # @return [Boolean] True if has attachments
221
+ def has_attachments?
222
+ attachments.any?
223
+ end
224
+
225
+ # Check if message has reactions
226
+ # @return [Boolean] True if has reactions
227
+ def has_reactions?
228
+ reactions.any?
229
+ end
230
+
231
+ # Get total reaction count
232
+ # @return [Integer] Total reactions
233
+ def reaction_count
234
+ reactions.sum(&:count)
235
+ end
236
+
237
+ # Check if message is a system message
238
+ # @return [Boolean] True if system message
239
+ def system?
240
+ type != 0 && type != 19 && type != 20 && type != 23
241
+ end
242
+
243
+ # Check if message was deleted
244
+ # @return [Boolean] True if deleted (not present in data)
245
+ def deleted?
246
+ @raw_data['deleted'] || false
247
+ end
248
+
249
+ # Check if message has components (buttons, select menus)
250
+ # @return [Boolean] True if has components
251
+ def has_components?
252
+ components.any?
253
+ end
254
+
255
+ # Check if message has a poll
256
+ # @return [Boolean] True if has poll
257
+ def has_poll?
258
+ !poll.nil?
259
+ end
260
+
261
+ # Get sticker items
262
+ # @return [Array<Sticker>] Sticker items
263
+ def stickers
264
+ (@raw_data['sticker_items'] || @raw_data['stickers'] || []).map { |s| Sticker.new(s) }
265
+ end
266
+
267
+ # Get message flags
268
+ # @return [MessageFlags] Flags object
269
+ def message_flags
270
+ MessageFlags.new(flags)
271
+ end
272
+
273
+ # Get thread associated with this message
274
+ # @return [Channel, nil] Thread if created from this message
275
+ def thread
276
+ return nil unless @raw_data['thread']
277
+
278
+ Channel.new(@raw_data['thread'])
279
+ end
280
+
281
+ # Get application
282
+ # @return [Application, nil] Application
283
+ def application
284
+ return nil unless @raw_data['application']
285
+
286
+ Application.new(@raw_data['application'])
287
+ end
288
+
289
+ # Get resolved data for interactions
290
+ # @return [ResolvedData, nil] Resolved data
291
+ def resolved_data
292
+ return nil unless @raw_data['resolved']
293
+
294
+ ResolvedData.new(@raw_data['resolved'])
295
+ end
296
+
297
+ # Get position in thread
298
+ # @return [Integer, nil] Position
299
+ def position
300
+ @raw_data['position']
301
+ end
302
+
303
+ # Respond to this message (send reply)
304
+ # @param content [String] Message content
305
+ # @param options [Hash] Additional options (embeds, components, etc.)
306
+ # @yieldparam builder [MessageBuilder] Optional builder block
307
+ # @return [Message] The sent message
308
+ def respond(content = nil, **options, &block)
309
+ raise 'API client not configured. Call Bot#initialize first.' unless self.class.api
310
+
311
+ payload = { content: content }.merge(options).compact
312
+ payload[:message_reference] = {
313
+ message_id: id.to_s,
314
+ channel_id: channel_id.to_s,
315
+ guild_id: @raw_data['guild_id'],
316
+ fail_if_not_exists: false
317
+ }
318
+
319
+ # Execute builder block if given
320
+ if block
321
+ builder = MessageBuilder.new(payload)
322
+ block.call(builder)
323
+ payload = builder.to_h
324
+ end
325
+
326
+ data = self.class.api.post("/channels/#{channel_id}/messages", body: payload)
327
+ Message.new(data)
328
+ end
329
+
330
+ # React to this message with an emoji
331
+ # @param emoji [String, Emoji] Emoji to react with (can be unicode emoji or emoji ID string)
332
+ # @return [void]
333
+ def react(emoji)
334
+ raise 'API client not configured. Call Bot#initialize first.' unless self.class.api
335
+
336
+ emoji_string = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
337
+ emoji_encoded = CGI.escape(emoji_string)
338
+
339
+ self.class.api.put("/channels/#{channel_id}/messages/#{id}/reactions/#{emoji_encoded}/@me")
340
+ end
341
+
342
+ # Delete this message
343
+ # @param reason [String] Audit log reason
344
+ # @return [void]
345
+ def delete(reason: nil)
346
+ raise 'API client not configured. Call Bot#initialize first.' unless self.class.api
347
+
348
+ headers = {}
349
+ headers['X-Audit-Log-Reason'] = CGI.escape(reason) if reason
350
+
351
+ self.class.api.delete("/channels/#{channel_id}/messages/#{id}", headers: headers)
352
+ end
353
+
354
+ # Pin this message
355
+ # @param reason [String] Audit log reason
356
+ # @return [void]
357
+ def pin(reason: nil)
358
+ raise 'API client not configured. Call Bot#initialize first.' unless self.class.api
359
+
360
+ headers = {}
361
+ headers['X-Audit-Log-Reason'] = CGI.escape(reason) if reason
362
+
363
+ self.class.api.put("/channels/#{channel_id}/pins/#{id}", headers: headers)
364
+ end
365
+
366
+ # Unpin this message
367
+ # @param reason [String] Audit log reason
368
+ # @return [void]
369
+ def unpin(reason: nil)
370
+ raise 'API client not configured. Call Bot#initialize first.' unless self.class.api
371
+
372
+ headers = {}
373
+ headers['X-Audit-Log-Reason'] = CGI.escape(reason) if reason
374
+
375
+ self.class.api.delete("/channels/#{channel_id}/pins/#{id}", headers: headers)
376
+ end
377
+
378
+ # Edit this message
379
+ # @param content [String] New content
380
+ # @param options [Hash] Additional options (embeds, components, etc.)
381
+ # @yieldparam builder [MessageBuilder] Optional builder block
382
+ # @return [Message] The edited message
383
+ def edit(content = nil, **options, &block)
384
+ raise 'API client not configured. Call Bot#initialize first.' unless self.class.api
385
+
386
+ payload = { content: content }.merge(options).compact
387
+
388
+ # Execute builder block if given
389
+ if block
390
+ builder = MessageBuilder.new(payload)
391
+ block.call(builder)
392
+ payload = builder.to_h
393
+ end
394
+
395
+ data = self.class.api.patch("/channels/#{channel_id}/messages/#{id}", body: payload)
396
+ Message.new(data)
397
+ end
398
+
399
+ # Get creation time
400
+ # @return [Time] Message creation time
401
+ def created_at
402
+ timestamp
403
+ end
404
+ end
405
+ end