diff --git a/lib/discordrb/api/channel.rb b/lib/discordrb/api/channel.rb index fd18f7a5da..4a6665e13c 100644 --- a/lib/discordrb/api/channel.rb +++ b/lib/discordrb/api/channel.rb @@ -90,8 +90,8 @@ def message(token, channel_id, message_id) # https://discord.com/developers/docs/resources/channel#create-message # @param attachments [Array, nil] Attachments to use with `attachment://` in embeds. See # https://discord.com/developers/docs/resources/channel#create-message-using-attachments-within-embeds - def create_message(token, channel_id, message, tts = false, embeds = nil, nonce = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = nil, enforce_nonce = false, poll = nil) - body = { content: message, tts: tts, embeds: embeds, nonce: nonce, allowed_mentions: allowed_mentions, message_reference: message_reference, components: components&.to_a, flags: flags, enforce_nonce: enforce_nonce, poll: poll } + def create_message(token, channel_id, message, tts = false, embeds = nil, nonce = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = nil, enforce_nonce = false, poll = nil, shared_client_theme = nil) + body = { content: message, tts: tts, embeds: embeds, nonce: nonce, allowed_mentions: allowed_mentions, message_reference: message_reference, components: components&.to_a, flags: flags, enforce_nonce: enforce_nonce, poll: poll, shared_client_theme: shared_client_theme } body = if attachments files = [*0...attachments.size].zip(attachments).to_h { **files, payload_json: body.to_json } diff --git a/lib/discordrb/bot.rb b/lib/discordrb/bot.rb index a3e66f1f83..b473512c56 100644 --- a/lib/discordrb/bot.rb +++ b/lib/discordrb/bot.rb @@ -422,15 +422,16 @@ def delete_invite(code) # @param nonce [String, nil] A optional nonce in order to verify that a message was sent. Maximum of twenty-five characters. # @param enforce_nonce [true, false] Whether the nonce should be enforced and used for message de-duplication. # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to this message. + # @param shared_theme [hash, SharedTheme::Builder, SharedTheme, nil] The client-side theme to share via this message. # @return [Message] The message that was sent. - def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil) + def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil, shared_theme = nil) channel = channel.resolve_id debug("Sending message to #{channel} with content '#{content}'") allowed_mentions = { parse: [] } if allowed_mentions == false message_reference = { message_id: message_reference.resolve_id } if message_reference.respond_to?(:resolve_id) embeds = (embeds.instance_of?(Array) ? embeds.map(&:to_hash) : [embeds&.to_hash]).compact - response = API::Channel.create_message(token, channel, content, tts, embeds, nonce, attachments, allowed_mentions&.to_hash, message_reference, components, flags, enforce_nonce, poll&.to_h) + response = API::Channel.create_message(token, channel, content, tts, embeds, nonce, attachments, allowed_mentions&.to_hash, message_reference, components, flags, enforce_nonce, poll&.to_h, shared_theme&.to_h) Message.new(JSON.parse(response), self) end @@ -449,11 +450,13 @@ def send_message(channel, content, tts = false, embeds = nil, attachments = nil, # @param nonce [String, nil] A optional nonce in order to verify that a message was sent. Maximum of twenty-five characters. # @param enforce_nonce [true, false] Whether the nonce should be enforced and used for message de-duplication. # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to this message. - def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil) + # @param shared_theme [hash, SharedTheme::Builder, SharedTheme, nil] The client-side theme to share via this message. + # @return [nil] + def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil, shared_theme = nil) Thread.new do Thread.current[:discordrb_name] = "#{@current_thread}-temp-msg" - message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce, poll) + message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce, poll, shared_theme) sleep(timeout) message.delete end diff --git a/lib/discordrb/colour_rgb.rb b/lib/discordrb/colour_rgb.rb index 8b7ed3a116..f83b286e79 100644 --- a/lib/discordrb/colour_rgb.rb +++ b/lib/discordrb/colour_rgb.rb @@ -25,7 +25,7 @@ class ColourRGB # @example Initialize a with a hexadecimal string # ColourRGB.new('7289da') #=> ColourRGB def initialize(combined) - @combined = combined.is_a?(String) ? combined.to_i(16) : combined + @combined = combined.is_a?(String) ? combined.delete('#').to_i(16) : combined @red = (@combined >> 16) & 0xFF @green = (@combined >> 8) & 0xFF @blue = @combined & 0xFF @@ -35,7 +35,18 @@ def initialize(combined) def hex @combined.to_s(16) end + + alias_method :to_s, :hex alias_method :hexadecimal, :hex + + # Check if two colour RGB objects are equivalent. + # @param other [Object] The object to compare against for equality. + # @return [true, false] Whether or not the two objects are equivalent. + def ==(other) + other.is_a?(ColourRGB) ? @combined == other.combined : false + end + + alias_method :eql?, :== end # Alias for the class {ColourRGB} diff --git a/lib/discordrb/data.rb b/lib/discordrb/data.rb index f7687441a9..8584d5f3e7 100644 --- a/lib/discordrb/data.rb +++ b/lib/discordrb/data.rb @@ -55,3 +55,4 @@ require 'discordrb/data/timestamp' require 'discordrb/data/scheduled_event' require 'discordrb/data/poll' +require 'discordrb/data/shared_theme' diff --git a/lib/discordrb/data/channel.rb b/lib/discordrb/data/channel.rb index 0b387fb343..3bd76ad90a 100644 --- a/lib/discordrb/data/channel.rb +++ b/lib/discordrb/data/channel.rb @@ -611,16 +611,18 @@ def send_embed(message = '', embed = nil, attachments = nil, tts = false, allowe # @param nonce [nil, String, Integer, false] The 25 character nonce that should be used when sending this message. # @param enforce_nonce [true, false] Whether the provided nonce should be enforced and used for message de-duplication. # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to the message. + # @param shared_theme [hash, SharedTheme::Builder, SharedTheme, nil] The client-side theme to share via the message. # @yieldparam builder [Webhooks::Builder] An optional message builder. Arguments passed to the builder overwrite method data. # @yieldparam view [Webhooks::View] An optional component builder. Arguments passed to the builder overwrite method data. # @return [Message, nil] The resulting message that was created, or `nil` if the `timeout` parameter was set to a non `nil` value. - def send_message!(content: '', timeout: nil, tts: false, embeds: [], attachments: nil, allowed_mentions: nil, reference: nil, components: nil, flags: 0, has_components: false, nonce: nil, enforce_nonce: false, poll: nil) + def send_message!(content: '', timeout: nil, tts: false, embeds: [], attachments: nil, allowed_mentions: nil, reference: nil, components: nil, flags: 0, has_components: false, nonce: nil, enforce_nonce: false, poll: nil, shared_theme: nil) builder = Discordrb::Webhooks::Builder.new view = Discordrb::Webhooks::View.new builder.tts = tts builder.poll = poll builder.content = content + builder.shared_theme = shared_theme embeds&.each { |embed| builder << embed } builder.allowed_mentions = allowed_mentions @@ -631,9 +633,9 @@ def send_message!(content: '', timeout: nil, tts: false, embeds: [], attachments builder = builder.to_json_hash if timeout - @bot.send_temporary_message(@id, builder[:content], timeout, builder[:tts], builder[:embeds], attachments, builder[:allowed_mentions], reference, components&.to_a || view.to_a, flags, nonce, enforce_nonce, builder[:poll]) + @bot.send_temporary_message(@id, builder[:content], timeout, builder[:tts], builder[:embeds], attachments, builder[:allowed_mentions], reference, components&.to_a || view.to_a, flags, nonce, enforce_nonce, builder[:poll], builder[:shared_client_theme]) else - @bot.send_message(@id, builder[:content], builder[:tts], builder[:embeds], attachments, builder[:allowed_mentions], reference, components&.to_a || view.to_a, flags, nonce, enforce_nonce, builder[:poll]) + @bot.send_message(@id, builder[:content], builder[:tts], builder[:embeds], attachments, builder[:allowed_mentions], reference, components&.to_a || view.to_a, flags, nonce, enforce_nonce, builder[:poll], builder[:shared_client_theme]) end end diff --git a/lib/discordrb/data/message.rb b/lib/discordrb/data/message.rb index 82354bc86b..e535b733d4 100644 --- a/lib/discordrb/data/message.rb +++ b/lib/discordrb/data/message.rb @@ -148,6 +148,9 @@ class Message # @return [Integer] a generally increasing integer that can be used to determine this message's position in a thread. attr_reader :position + # @return [SharedTheme, nil] the client-side theme that was shared via this message, or `nil`. + attr_reader :shared_theme + # @return [Poll, nil] the poll that was sent with this message, or `nil`. attr_reader :poll @@ -215,6 +218,7 @@ def initialize(data, bot) @pinned_at = Time.parse(data['pinned_at']) if data['pinned_at'] @call = Call.new(data['call'], @bot) if data['call'] @poll = Poll.new(data['poll'], self, @bot) if data['poll'] + @shared_theme = SharedTheme.new(data['shared_client_theme'], @bot) if data['shared_client_theme'] @snapshots = data['message_snapshots']&.map { |snapshot| Snapshot.new(snapshot['message'], @bot) } || [] @role_subscription = RoleSubscriptionData.new(data['role_subscription_data'], self, @bot) if data['role_subscription_data'] @@ -375,7 +379,7 @@ def webhook? # @return [Array] the emotes that were used/mentioned in this message. def emoji - return [] if @content.empty? || @content.nil? + return [] if @content.nil? || @content.empty? @emoji ||= @bot.parse_mentions(@content).select { |mention| mention.is_a?(Discordrb::Emoji) } end diff --git a/lib/discordrb/data/shared_theme.rb b/lib/discordrb/data/shared_theme.rb new file mode 100644 index 0000000000..ffed0c358c --- /dev/null +++ b/lib/discordrb/data/shared_theme.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +module Discordrb + # A theme for the official Discord client. + class SharedTheme + # Mapping of names to base theme values. + BASES = { + unset: 0, + dark: 1, + light: 2, + darker: 3, + midnight: 4 + }.freeze + + # @return [Integer] the background tone of the theme. + attr_reader :base + + # @return [Integer] the angle of the theme's colours. + attr_reader :angle + + # @return [Array] the colours of the theme. + attr_reader :colours + alias colors colours + + # @return [Integer] the intensity of the theme's colours. + attr_reader :intensity + + # @!visibility private + def initialize(data, bot) + @bot = bot + @base = data['base_theme'] + @intensity = data['base_mix'] + @angle = data['gradient_angle'] + @colours = data['colors']&.map { |value| ColourRGB.new(value) } + end + + # Check if two shared theme objects are equivalent. + # @param other [Object] The object to compare against for equality. + # @return [true, false] Whether or not the two objects are equivalent. + def ==(other) + return false unless other.is_a?(SharedTheme) + + @angle == other.angle && @base == other.base && + @intensity == other.intensity && @colours == other.colours + end + + alias_method :eql?, :== + + # @!method unset_base? + # @return [true, false] whether or not the background tone of the theme is not defined. + # @!method dark_base? + # @return [true, false] whether or not the background tone of the theme is a dark color. + # @!method light_base? + # @return [true, false] whether or not the background tone of the theme is a light color. + # @!method darker_base? + # @return [true, false] whether or not the background tone of the theme is a darker color. + # @!method midnight_base? + # @return [true, false] whether or not the background tone of the theme is a midnight color. + BASES.each do |name, value| + define_method("#{name}_base?") { @base == value } + end + + # @!visibility private + def to_h + { + base_theme: @base, + base_mix: @intensity, + gradient_angle: @angle, + colors: @colours.map { |value| format('%06x', value.to_i) } + } + end + + # @!visibility private + def inspect + "" + end + + # Builder for shared themes. + class Builder + # Create a shared theme object. + # @param angle [Integer] The angle of the theme's colours; between 0-360. + # @param intensity [Integer] The intensity of the theme's colours; between 0-100. + # @param base [Integer, Symbol, nil] The background tone of the theme; see {BASES}. + def initialize(angle:, intensity:, base: :unset) + @base = base + @colours = [] + @angle = angle + @intensity = intensity + end + + # Add a colour to the shared theme. + # @param value [Integer, String, ColourRGB] The colour to add to the theme's colours. + # @return [void] + def colour(value) + raise 'Maximum number of shared theme colours reached (5)' if @colours.length == 5 + + @colours << format('%06x', value.is_a?(String) ? value&.delete('#')&.to_i(16) : value&.to_i) + end + + alias_method :color, :colour + alias_method :add_color, :colour + alias_method :add_colour, :colour + + # @!visibility private + def to_h + { + colors: @colours, + base_mix: @intensity.to_i, + gradient_angle: @angle.to_i, + base_theme: BASES[@base] || @base + } + end + end + end +end diff --git a/lib/discordrb/webhooks/builder.rb b/lib/discordrb/webhooks/builder.rb index ff904a95a4..4872e31cf8 100644 --- a/lib/discordrb/webhooks/builder.rb +++ b/lib/discordrb/webhooks/builder.rb @@ -5,7 +5,7 @@ module Discordrb::Webhooks # A class that acts as a builder for a webhook message object. class Builder - def initialize(content: '', username: nil, avatar_url: nil, tts: false, file: nil, embeds: [], allowed_mentions: nil, poll: nil) + def initialize(content: '', username: nil, avatar_url: nil, tts: false, file: nil, embeds: [], allowed_mentions: nil, poll: nil, shared_theme: nil) @content = content @username = username @avatar_url = avatar_url @@ -14,6 +14,7 @@ def initialize(content: '', username: nil, avatar_url: nil, tts: false, file: ni @embeds = embeds @allowed_mentions = allowed_mentions @poll = poll + @shared_theme = shared_theme end # The content of the message. May be 2000 characters long at most. @@ -75,8 +76,8 @@ def add_embed(embed = nil) # end # @param poll [Poll::Builder, Poll, Hash, nil] The poll to start the building process with, or nil if one should be created anew. # @return [Poll::Builder, Poll] The created poll. - def add_poll(poll = nil, **kwargs) - poll ||= Discordrb::Poll::Builder.new(**kwargs) + def add_poll(poll = nil, ...) + poll ||= Discordrb::Poll::Builder.new(...) yield(poll) if block_given? @poll = poll poll @@ -84,6 +85,24 @@ def add_poll(poll = nil, **kwargs) alias_method :poll, :add_poll + # Convenience method to add a shared theme using a builder pattern + # @example Add a shared theme to a message + # builder.shared_theme(angle: 257, intensity: 20) do |theme| + # theme.colour('C27F74') + # theme.colour('0F0508') + # theme.colour('BF7E72') + # end + # @param theme [SharedTheme::Builder, SharedTheme, Hash, nil] The theme to start the building process with, or nil if one should be created anew. + # @return [SharedTheme::Builder, SharedTheme] The created theme. + def add_shared_theme(theme = nil, ...) + theme ||= Discordrb::SharedTheme::Builder.new(...) + yield(theme) if block_given? + @shared_theme = theme + theme + end + + alias_method :shared_theme, :add_shared_theme + # @return [File, nil] the file attached to this message. attr_reader :file @@ -98,6 +117,10 @@ def add_poll(poll = nil, **kwargs) # @see https://discord.com/developers/docs/resources/poll#poll-create-request-object attr_writer :poll + # @return [hash, SharedTheme::Builder, SharedTheme, nil] The client-side theme to share via this message. + # @see https://docs.discord.com/developers/resources/message#shared-client-theme-object + attr_writer :shared_theme + # @return [Hash] a hash representation of the created message, for JSON format. def to_json_hash { @@ -107,7 +130,8 @@ def to_json_hash tts: @tts, embeds: @embeds.map(&:to_hash), allowed_mentions: @allowed_mentions&.to_hash, - poll: @poll&.to_h + poll: @poll&.to_h, + shared_client_theme: @shared_theme&.to_h } end