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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +398 -0
- data/lib/discord_rda/bot.rb +842 -0
- data/lib/discord_rda/cache/configurable_cache.rb +283 -0
- data/lib/discord_rda/cache/entity_cache.rb +184 -0
- data/lib/discord_rda/cache/memory_store.rb +143 -0
- data/lib/discord_rda/cache/redis_store.rb +136 -0
- data/lib/discord_rda/cache/store.rb +56 -0
- data/lib/discord_rda/connection/gateway_client.rb +383 -0
- data/lib/discord_rda/connection/invalid_bucket.rb +205 -0
- data/lib/discord_rda/connection/rate_limiter.rb +280 -0
- data/lib/discord_rda/connection/request_queue.rb +340 -0
- data/lib/discord_rda/connection/reshard_manager.rb +328 -0
- data/lib/discord_rda/connection/rest_client.rb +316 -0
- data/lib/discord_rda/connection/rest_proxy.rb +165 -0
- data/lib/discord_rda/connection/scalable_rest_client.rb +526 -0
- data/lib/discord_rda/connection/shard_manager.rb +223 -0
- data/lib/discord_rda/core/async_runtime.rb +108 -0
- data/lib/discord_rda/core/configuration.rb +194 -0
- data/lib/discord_rda/core/logger.rb +188 -0
- data/lib/discord_rda/core/snowflake.rb +121 -0
- data/lib/discord_rda/entity/attachment.rb +88 -0
- data/lib/discord_rda/entity/base.rb +103 -0
- data/lib/discord_rda/entity/channel.rb +446 -0
- data/lib/discord_rda/entity/channel_builder.rb +280 -0
- data/lib/discord_rda/entity/color.rb +253 -0
- data/lib/discord_rda/entity/embed.rb +221 -0
- data/lib/discord_rda/entity/emoji.rb +89 -0
- data/lib/discord_rda/entity/factory.rb +99 -0
- data/lib/discord_rda/entity/guild.rb +619 -0
- data/lib/discord_rda/entity/member.rb +263 -0
- data/lib/discord_rda/entity/message.rb +405 -0
- data/lib/discord_rda/entity/message_builder.rb +369 -0
- data/lib/discord_rda/entity/role.rb +157 -0
- data/lib/discord_rda/entity/support.rb +294 -0
- data/lib/discord_rda/entity/user.rb +231 -0
- data/lib/discord_rda/entity/value_objects.rb +263 -0
- data/lib/discord_rda/event/auto_moderation.rb +294 -0
- data/lib/discord_rda/event/base.rb +986 -0
- data/lib/discord_rda/event/bus.rb +225 -0
- data/lib/discord_rda/event/scheduled_event.rb +257 -0
- data/lib/discord_rda/hot_reload_manager.rb +303 -0
- data/lib/discord_rda/interactions/application_command.rb +436 -0
- data/lib/discord_rda/interactions/command_system.rb +484 -0
- data/lib/discord_rda/interactions/components.rb +464 -0
- data/lib/discord_rda/interactions/interaction.rb +553 -0
- data/lib/discord_rda/plugin/analytics_plugin.rb +528 -0
- data/lib/discord_rda/plugin/base.rb +190 -0
- data/lib/discord_rda/plugin/registry.rb +126 -0
- data/lib/discord_rda/version.rb +5 -0
- data/lib/discord_rda.rb +70 -0
- 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
|