lib/discorb/message.rb
# frozen_string_literal: true
module Discorb
#
# Represents a message.
#
class Message < DiscordModel
# @return [Discorb::Snowflake] The ID of the message.
attr_reader :id
# @return [Discorb::User, Discorb::Member, Webhook::Message::Author] The user that sent the message.
attr_reader :author
# @return [String] The content of the message.
attr_reader :content
alias to_s content
# @return [Time] The time the message was created.
attr_reader :created_at
alias timestamp created_at
alias sent_at created_at
# @return [Time] The time the message was edited.
# @return [nil] If the message was not edited.
attr_reader :updated_at
alias edited_at updated_at
alias edited_timestamp updated_at
# @return [Array<Discorb::Attachment>] The attachments of the message.
attr_reader :attachments
# @return [Array<Discorb::Embed>] The embeds of the message.
attr_reader :embeds
# @return [Array<Discorb::Reaction>] The reactions of the message.
attr_reader :reactions
# @return [Discorb::Snowflake] The ID of the channel the message was sent in.
attr_reader :webhook_id
# @return [Symbol] The type of the message.
# Currently, this will be one of:
#
# * `:default`
# * `:recipient_add`
# * `:recipient_remove`
# * `:call`
# * `:channel_name_change`
# * `:channel_icon_change`
# * `:channel_pinned_message`
# * `:guild_member_join`
# * `:user_premium_guild_subscription`
# * `:user_premium_guild_subscription_tier_1`
# * `:user_premium_guild_subscription_tier_2`
# * `:user_premium_guild_subscription_tier_3`
# * `:channel_follow_add`
# * `:guild_discovery_disqualified`
# * `:guild_discovery_requalified`
# * `:guild_discovery_grace_period_initial_warning`
# * `:guild_discovery_grace_period_final_warning`
# * `:thread_created`
# * `:reply`
# * `:chat_input_command`
# * `:thread_starter_message`
# * `:guild_invite_reminder`
# * `:context_menu_command`
attr_reader :type
# @return [Discorb::Message::Activity] The activity of the message.
attr_reader :activity
# @return [Discorb::Application] The application of the message.
attr_reader :application_id
# @return [Discorb::Message::Reference] The reference of the message.
attr_reader :message_reference
# @return [Discorb::Message::Flag] The flag of the message.
# @see Discorb::Message::Flag
attr_reader :flag
# @return [Discorb::Message::Sticker] The sticker of the message.
attr_reader :stickers
# @return [Discorb::Message::Interaction] The interaction of the message.
attr_reader :interaction
# @return [Discorb::ThreadChannel] The thread channel of the message.
attr_reader :thread
# @return [Array<Array<Discorb::Component>>] The components of the message.
attr_reader :components
# @return [Boolean] Whether the message is deleted.
attr_reader :deleted
alias deleted? deleted
# @return [Boolean] Whether the message is tts.
attr_reader :tts
alias tts? tts
# @return [Boolean] Whether the message mentions everyone.
attr_reader :mention_everyone
alias mention_everyone? mention_everyone
# @return [Boolean] Whether the message is pinned.
attr_reader :pinned
alias pinned? pinned
# @private
# @return [{Integer => Symbol}] The mapping of message type.
MESSAGE_TYPE = {
0 => :default,
1 => :recipient_add,
2 => :recipient_remove,
3 => :call,
4 => :channel_name_change,
5 => :channel_icon_change,
6 => :channel_pinned_message,
7 => :guild_member_join,
8 => :user_premium_guild_subscription,
9 => :user_premium_guild_subscription_tier_1,
10 => :user_premium_guild_subscription_tier_2,
11 => :user_premium_guild_subscription_tier_3,
12 => :channel_follow_add,
14 => :guild_discovery_disqualified,
15 => :guild_discovery_requalified,
16 => :guild_discovery_grace_period_initial_warning,
17 => :guild_discovery_grace_period_final_warning,
18 => :thread_created,
19 => :reply,
20 => :chat_input_command,
21 => :thread_starter_message,
22 => :guild_invite_reminder,
23 => :context_menu_command
}.freeze
# @!attribute [r] channel
# @macro client_cache
# @return [Discorb::Channel] The channel the message was sent in.
# @!attribute [r] guild
# @macro client_cache
# @return [Discorb::Guild] The guild the message was sent in.
# @return [nil] If the message was not sent in a guild.
# @!attribute [r] webhook?
# @return [Boolean] Whether the message was sent by a webhook.
# @!attribute [r] edited?
# @return [Boolean] Whether the message was edited.
# @!attribute [r] jump_url
# @return [String] The URL to jump to the message.
# @!attribute [r] embed
# @return [Discorb::Embed] The embed of the message.
# @return [nil] If the message has no embed.
# @!attribute [r] embed?
# @return [Boolean] Whether the message has an embed.
# @!attribute [r] reply?
# @return [Boolean] Whether the message is a reply.
# @!attribute [r] dm?
# @return [Boolean] Whether the message was sent in a DM.
# @!attribute [r] guild?
# @return [Boolean] Whether the message was sent in a guild.
def embed?
@embeds.any?
end
def reply?
!@message_reference.nil?
end
def dm?
@guild_id.nil?
end
def guild?
!@guild_id.nil?
end
#
# Initialize a new message.
# @private
#
# @param [Discorb::Client] client The client.
# @param [Hash] data The data of the welcome screen.
# @param [Boolean] no_cache Whether to disable caching.
#
def initialize(client, data, no_cache: false)
@client = client
@data = {}
@no_cache = no_cache
_set_data(data)
@client.messages[@id] = self unless @no_cache
end
def channel
@dm || @client.channels[@channel_id]
end
def guild
@client.guilds[@guild_id]
end
def webhook?
@webhook_id != false
end
def jump_url
"https://discord.com/channels/#{@guild_id || "@me"}/#{@channel_id}/#{@id}"
end
def edited?
!@updated_at.nil?
end
#
# Removes the mentions from the message.
#
# @param [Boolean] user Whether to clean user mentions.
# @param [Boolean] channel Whether to clean channel mentions.
# @param [Boolean] role Whether to clean role mentions.
# @param [Boolean] emoji Whether to clean emoji.
# @param [Boolean] everyone Whether to clean `@everyone` and `@here`.
# @param [Boolean] codeblock Whether to clean codeblocks.
#
# @return [String] The cleaned content of the message.
#
def clean_content(
user: true,
channel: true,
role: true,
emoji: true,
everyone: true,
codeblock: false
)
ret = @content.dup
if user
ret.gsub!(/<@!?(\d+)>/) do |_match|
member = guild&.members&.[](Regexp.last_match(1))
member ||= @client.users[Regexp.last_match(1)]
member ? "@#{member.name}" : "@Unknown User"
end
end
ret.gsub!(/<#(\d+)>/) do |_match|
channel = @client.channels[Regexp.last_match(1)]
channel ? "<##{channel.id}>" : "#Unknown Channel"
end
if role
ret.gsub!(/<@&(\d+)>/) do |_match|
r = guild&.roles&.[](Regexp.last_match(1))
r ? "@#{r.name}" : "@Unknown Role"
end
end
if emoji
ret.gsub!(/<a?:([a-zA-Z0-9_]+):\d+>/) { |_match| Regexp.last_match(1) }
end
ret.gsub!(/@(everyone|here)/, "@\u200b\\1") if everyone
if codeblock
ret
else
codeblocks = ret.split("```", -1)
original_codeblocks = @content.scan(/```(.+?)```/m)
res = []
max = codeblocks.length
codeblocks.each_with_index do |single_codeblock, i|
res << if (max.even? && i == max - 1) || i.even?
single_codeblock
else
original_codeblocks[i / 2]
end
end
res.join("```")
end
end
#
# Edit the message.
# @async
#
# @param [String] content The message content.
# @param [Discorb::Embed] embed The embed to send.
# @param [Array<Discorb::Embed>] embeds The embeds to send.
# @param [Discorb::AllowedMentions] allowed_mentions The allowed mentions.
# @param [Array<Discorb::Attachment>] attachments The new attachments.
# @param [Array<Discorb::Component>, Array<Array<Discorb::Component>>] components The components to send.
# @param [Boolean] supress Whether to supress embeds.
#
# @return [Async::Task<void>] The task.
#
def edit(
content = Discorb::Unset,
embed: Discorb::Unset,
embeds: Discorb::Unset,
allowed_mentions: Discorb::Unset,
attachments: Discorb::Unset,
components: Discorb::Unset,
supress: Discorb::Unset
)
Async do
channel.edit_message(
@id,
content,
embed:,
embeds:,
allowed_mentions:,
attachments:,
components:,
supress:
).wait
end
end
#
# Delete the message.
# @async
#
# @param [String] reason The reason for deleting the message.
#
# @return [Async::Task<void>] The task.
#
def delete(reason: nil)
Async { channel.delete_message(@id, reason:).wait }
end
#
# Convert the message to reference object.
#
# @param [Boolean] fail_if_not_exists Whether to raise an error if the message does not exist.
#
# @return [Discorb::Message::Reference] The reference object.
#
def to_reference(fail_if_not_exists: true)
Reference.from_hash(
{
message_id: @id,
channel_id: @channel_id,
guild_id: @guild_id,
fail_if_not_exists:
}
)
end
def embed
@embeds[0]
end
# Reply to the message.
# @async
# @param (see #post)
# @return [Async::Task<Discorb::Message>] The message.
def reply(*args, **kwargs)
Async { channel.post(*args, reference: self, **kwargs).wait }
end
#
# Publish the message.
# @async
#
# @return [Async::Task<void>] The task.
#
def publish
Async do
channel.post(
"/channels/#{@channel_id}/messages/#{@id}/crosspost",
nil
).wait
end
end
#
# Add a reaction to the message.
# @async
#
# @param [Discorb::Emoji] emoji The emoji to react with.
#
# @return [Async::Task<void>] The task.
#
def add_reaction(emoji)
Async do
@client
.http
.request(
Route.new(
"/channels/#{@channel_id}/messages/#{@id}/reactions/#{emoji.to_uri}/@me",
"//channels/:channel_id/messages/:message_id/reactions/:emoji/@me",
:put
),
nil
)
.wait
end
end
alias react_with add_reaction
#
# Remove a reaction from the message.
# @async
#
# @param [Discorb::Emoji] emoji The emoji to remove.
#
# @return [Async::Task<void>] The task.
#
def remove_reaction(emoji)
Async do
@client
.http
.request(
Route.new(
"/channels/#{@channel_id}/messages/#{@id}/reactions/#{emoji.to_uri}/@me",
"//channels/:channel_id/messages/:message_id/reactions/:emoji/@me",
:delete
)
)
.wait
end
end
alias delete_reaction remove_reaction
#
# Remove other member's reaction from the message.
# @async
#
# @param [Discorb::Emoji] emoji The emoji to remove.
# @param [Discorb::Member] member The member to remove the reaction from.
#
# @return [Async::Task<void>] The task.
#
def remove_reaction_of(emoji, member)
Async do
@client
.http
.request(
Route.new(
"/channels/#{@channel_id}/messages/#{@id}/reactions/#{emoji.to_uri}/#{
member.is_a?(Member) ? member.id : member
}",
"//channels/:channel_id/messages/:message_id/reactions/:emoji/:user_id",
:delete
)
)
.wait
end
end
alias delete_reaction_of remove_reaction_of
#
# Fetch reacted users of reaction.
# @async
#
# @param [Discorb::Emoji, Discorb::PartialEmoji] emoji The emoji to fetch.
# @param [Integer, nil] limit The maximum number of users to fetch. `nil` for no limit.
# @param [Discorb::Snowflake, nil] after The ID of the user to start fetching from.
#
# @return [Async::Task<Array<Discorb::User>>] The users.
#
def fetch_reacted_users(
emoji,
limit: nil,
after: Discorb::Snowflake.new("0")
)
Async do
if limit.nil? || !limit.positive?
after = Discorb::Snowflake.new("0")
users = []
loop do
_resp, data =
@client
.http
.request(
Route.new(
"/channels/#{@channel_id}/messages/#{@id}/reactions/#{emoji.to_uri}?limit=100&after=#{after}",
"//channels/:channel_id/messages/:message_id/reactions/:emoji",
:get
)
)
.wait
break if data.empty?
users +=
data.map do |r|
guild&.members&.[](r[:id]) || @client.users[r[:id]] ||
User.new(@client, r)
end
break if data.length < 100
after = data[-1][:id]
end
next users
else
_resp, data =
@client
.http
.request(
Route.new(
"/channels/#{@channel_id}/messages/#{@id}/reactions/#{emoji.to_uri}?limit=#{limit}&after=#{after}",
"//channels/:channel_id/messages/:message_id/reactions/:emoji",
:get
)
)
.wait
next(
data.map do |r|
guild&.members&.[](r[:id]) || @client.users[r[:id]] ||
User.new(@client, r)
end
)
end
end
end
#
# Pin the message.
# @async
#
# @param [String] reason The reason for pinning the message.
#
# @return [Async::Task<void>] The task.
#
def pin(reason: nil)
Async { channel.pin_message(self, reason:).wait }
end
#
# Unpin the message.
# @async
#
# @param [String] reason The reason for unpinning the message.
#
# @return [Async::Task<void>] The task.
#
def unpin(reason: nil)
Async { channel.unpin_message(self, reason:).wait }
end
#
# Start thread from the message.
# @async
#
# @param (see Discorb::Channel#start_thread)
#
# @return [Async::Task<Discorb::ThreadChannel>] <description>
#
def start_thread(*args, **kwargs)
Async { channel.start_thread(*args, message: self, **kwargs).wait }
end
alias create_thread start_thread
# Meta
def inspect
"#<#{self.class} #{@content.inspect} id=#{@id}>"
end
private
def _set_data(data)
@id = Snowflake.new(data[:id])
@channel_id = data[:channel_id]
if data[:guild_id]
@guild_id = data[:guild_id]
@dm = nil
else
@dm = Discorb::DMChannel.new(@client, data[:channel_id])
@guild_id = nil
end
if data[:member].nil? && data[:webhook_id]
@webhook_id = Snowflake.new(data[:webhook_id])
@author = Webhook::Message::Author.new(data[:author])
elsif data[:guild_id].nil? || data[:guild_id].empty? || data[:member].nil?
@author =
@client.users[data[:author][:id]] || User.new(@client, data[:author])
else
@author =
guild&.members&.get(data[:author][:id]) ||
Member.new(@client, @guild_id, data[:author], data[:member])
end
@content = data[:content]
@created_at = Time.iso8601(data[:timestamp])
@updated_at =
(
if data[:edited_timestamp].nil?
nil
else
Time.iso8601(data[:edited_timestamp])
end
)
@tts = data[:tts]
@mention_everyone = data[:mention_everyone]
@mention_roles = data[:mention_roles].map { |r| guild.roles[r] }
@attachments = data[:attachments].map { |a| Attachment.from_hash(a) }
@embeds =
data[:embeds] ? data[:embeds].map { |e| Embed.from_hash(e) } : []
@reactions =
(
if data[:reactions]
data[:reactions].map { |r| Reaction.new(self, r) }
else
[]
end
)
@pinned = data[:pinned]
@type = MESSAGE_TYPE[data[:type]]
@activity = data[:activity] && Activity.new(data[:activity])
@application_id = data[:application_id]
@message_reference =
data[:message_reference] &&
Reference.from_hash(data[:message_reference])
@flag = Flag.new(0b111 - data[:flags])
@sticker_items =
(
if data[:sticker_items]
data[:sticker_items].map { |s| Message::Sticker.new(s) }
else
[]
end
)
# @referenced_message = data[:referenced_message] && Message.new(@client, data[:referenced_message])
@interaction =
data[:interaction] &&
Message::Interaction.new(@client, data[:interaction])
@thread = data[:thread] && Channel.make_channel(@client, data[:thread])
@components =
data[:components].map do |c|
c[:components].map { |co| Component.from_hash(co) }
end
@data.update(data)
@deleted = false
end
end
end