Shimogawa/rubirai

View on GitHub
lib/rubirai/messages/message.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require 'rubirai/utils'

# @!method self.AtMessage(**kwargs)
#   Form an {Rubirai::AtMessage}. The `display` option has no effect when
#   sending at messages.
#   @option kwargs [Integer] :target the target id
#   @return [Rubirai::AtMessage] the message object
#   @see Rubirai::AtMessage.from
# @!method self.QuoteMessage(**kwargs)
#   Form a {Rubirai::QuoteMessage}.
#   @return [Rubirai::QuoteMessage] the message object
#   @see Rubirai::QuoteMessage.from
# @!method self.AtAllMessage()
#   Form an {Rubirai::AtAllMessage}.
#   @return [Rubirai::AtAllMessage] the message object
#   @see Rubirai::AtAllMessage.from
# @!method self.FaceMessage(**kwargs)
#   Form a {Rubirai::FaceMessage}. Only needs to give one of the two arguments.
#   @option kwargs [Integer] :face_id the face id (high priority)
#   @option kwargs [String] :name the face's name (low priority)
#   @return [Rubirai::FaceMessage] the message object
#   @see Rubirai::FaceMessage.from
# @!method self.PlainMessage(**kwargs)
#   @option kwargs [String] :text the plain text
#   @return [Rubirai::PlainMessage] the message object
#   @see Rubirai::PlainMessage.from
# @!method self.ImageMessage(**kwargs)
#   Form an {Rubirai::ImageMessage}. Only needs to give one of the three arguments.
#   @option kwargs [String] :image_id the image id
#   @option kwargs [String] :url the url of the image
#   @option kwargs [String] :path the local path of the image
#   @return [Rubirai::ImageMessage] the message object
#   @see Rubirai::ImageMessage.from
# @!method self.FlashImageMessage(**kwargs)
#   Form a {Rubirai::FlashImageMessage}. Only needs to give one of the three arguments.
#   @option kwargs [String] :image_id the image id
#   @option kwargs [String] :url the url of the image
#   @option kwargs [String] :path the local path of the image
#   @return [Rubirai::FlashImageMessage] the message object
#   @see Rubirai::FlashImageMessage.from
# @!method self.VoiceMessage(**kwargs)
#   Form a {Rubirai::VoiceMessage}. Only needs to give one of the three arguments.
#   @option kwargs [String] :voice_id the voice id
#   @option kwargs [String] :url the url of the voice
#   @option kwargs [String] :path the local path of the voice
#   @return [Rubirai::VoiceMessage] the message object
#   @see Rubirai::VoiceMessage.from
# @!method self.XmlMessage(**kwargs)
#   Form a {Rubirai::XmlMessage}.
#   @option kwargs [String] :xml the xml body
#   @return [Rubirai::XmlMessage] the message object
#   @see Rubirai::XmlMessage.from
# @!method self.JsonMessage(**kwargs)
#   Form a {Rubirai::JsonMessage}.
#   @option kwargs [String] :json the json body
#   @return [Rubirai::JsonMessage] the message object
#   @see Rubirai::JsonMessage.from
# @!method self.AppMessage(**kwargs)
#   Form an {Rubirai::AppMessage}.
#   @option kwargs [String] :content the app body
#   @return [Rubirai::AppMessage] the message object
#   @see Rubirai::AppMessage.from
# @!method self.PokeMessage(**kwargs)
#   Form a {Rubirai::PokeMessage}.
#   @option kwargs [String] :name the poke name
#   @return [Rubirai::PokeMessage] the message object
#   @see Rubirai::PokeMessage.from
module Rubirai
  # The message abstract class.
  #
  # @abstract
  class Message
    # @!attribute [r] bot
    #   @return [Bot] the bot
    # @!attribute [r] type
    #   @return [Symbol] the type
    attr_reader :bot, :type

    # Objects to {Rubirai::Message}
    #
    # @param msg [Rubirai::Message, Hash{String => Object}, Object] the object to transform to a message
    # @return [Rubirai::Message] the message
    def self.to_message(msg, bot = nil)
      # noinspection RubyYardReturnMatch
      case msg
      when Message, MessageChain
        msg
      when Hash
        Message.build_from(msg, bot)
      else
        PlainMessage.from(text: msg.to_s, bot: bot)
      end
    end

    # Get all message types (subclasses)
    #
    # @return [Array<Symbol>] all message types
    def self.all_types
      %i[
        Source Quote At AtAll Face Plain Image
        FlashImage Voice Xml Json App Poke Forward
        File MusicShare
      ]
    end

    # Check if a type is in all message types
    # @param type [Symbol] the type to check
    # @return whether the type is in all message types
    def self.check_type(type)
      raise(RubiraiError, 'type not in all message types') unless Message.all_types.include? type
    end

    # @private
    def self.get_msg_klass(type)
      Object.const_get "Rubirai::#{type}Message"
    end

    # @private
    def self.build_from(hash, bot = nil)
      hash = hash.stringify_keys
      raise(RubiraiError, 'not a valid message') unless hash.key? 'type'

      type = hash['type'].to_sym
      check_type type
      klass = get_msg_klass type
      klass.new hash, bot
    end

    # @private
    def self.set_message(type, *attr_keys, &initialize_block)
      attr_reader(*attr_keys)

      metaclass.instance_eval do
        define_method(:keys) do
          attr_keys
        end
        break if attr_keys.empty?
        define_method(:from) do |bot: nil, **kwargs|
          res = get_msg_klass(type).new({}, bot)
          attr_keys.each do |key|
            res.instance_variable_set "@#{key}", kwargs[key]
          end
          res
        end
        s = +"def from_with_param(#{attr_keys.join('= nil, ')} = nil)\n"
        s << "res = Rubirai::#{type}Message.new({})\n"
        attr_keys.each do |key|
          s << %[res.instance_variable_set("@#{key}", #{key})\n]
        end
        s << "res\nend"
        class_eval s
      end

      class_eval do
        define_method(:initialize) do |hash, bot = nil|
          # noinspection RubySuperCallWithoutSuperclassInspection
          super type, bot
          initialize_block&.call(hash)
          hash = hash.stringify_keys
          attr_keys.each do |k|
            instance_variable_set("@#{k}", hash[k.to_s.snake_to_camel(lower: true)])
          end
        end
      end
    end

    # @private
    def initialize(type, bot = nil)
      Message.check_type type
      @bot = bot
      @type = type
    end

    # Convert the message to a hash
    #
    # @return [Hash{String => Object}]
    def to_h
      res = self.class.keys.to_h do |k|
        v = instance_variable_get("@#{k}")
        k = k.to_s.snake_to_camel(lower: true)
        if v.is_a? MessageChain
          [k, v.to_a]
        elsif v&.respond_to?(:to_h)
          [k, v.to_h]
        else
          [k, v]
        end
      end
      res[:type] = @type.to_s
      res.compact.stringify_keys
    end

    # @private
    def self.metaclass
      class << self
        self
      end
    end
  end

  # {include:Rubirai::Message.to_message}
  # @param obj [Message, Hash{String => Object}, Object] the object
  # @return [Message] the message
  # @see Rubirai::Message.to_message
  def self.Message(obj, bot = nil)
    Message.to_message obj, bot
  end

  Message.all_types.each do |type|
    self.class.define_method("#{type}Message".to_sym) do |**kwargs|
      Message.get_msg_klass(type).from(**kwargs)
    end
  end

  # The source message type
  class SourceMessage < Message
    # @!attribute [r] id
    #   @return [Integer] the message (chain) id
    # @!attribute [r] time
    #   @return [Integer] the timestamp
    # @!method from(**kwargs)
    #   @option kwargs [Integer] :id the id
    #   @option kwargs [Integer] :time the timestamp
    #   @!scope class
    set_message :Source, :id, :time
  end

  # The quote message type
  class QuoteMessage < Message
    # @!attribute [r] id
    #   @return [Integer] the original (quoted) message chain id
    # @!attribute [r] group_id
    #   @return [Integer] the group id. `0` if from friend.
    # @!attribute [r] sender_id
    #   @return [Integer] the original sender's id
    # @!attribute [r] target_id
    #   @return [Integer] the original receiver's (group or user) id
    # @!attribute [r] origin
    #   @return [MessageChain] the original message chain
    # @!method from(**kwargs)
    #   Form a {QuoteMessage}.
    set_message :Quote, :id, :group_id, :sender_id, :target_id, :origin, :origin_raw

    # @private
    def initialize(hash, bot = nil)
      super :Quote, bot
      @id = hash['id']
      @group_id = hash['groupId']
      @sender_id = hash['senderId']
      @target_id = hash['targetId']
      @origin = MessageChain.make(*hash['origin'], bot: bot)
      @origin_raw = hash['origin']
    end

    def to_h
      {
        'type' => 'Quote',
        'id' => @id,
        'groupId' => @group_id,
        'senderId' => @sender_id,
        'targetId' => @target_id,
        'origin' => @origin_raw || @origin.to_a
      }.compact
    end
  end

  # The At message type
  class AtMessage < Message
    # @!attribute [r] target
    #   @return [Integer] the target group user's id
    # @!attribute [r] display
    #   @return [String] the displayed name (not used when sending)
    # @!method from(**kwargs)
    #   Form an {AtMessage}. The `display` option has no effect when
    #   sending at messages.
    #   @option kwargs [Integer] :target the target id
    #   @return [AtMessage] the message object
    set_message :At, :target, :display
  end

  # The At All message type
  class AtAllMessage < Message
    # @!method from()
    #   @return [AtAllMessage] the message object
    set_message :AtAll
  end

  # The QQ Face Emoji message type
  class FaceMessage < Message
    # @!attribute [r] face_id
    #   @return [Integer] the face's id
    # @!attribute [r] name
    #   @return [String, nil] the face's name
    # @!method from(**kwargs)
    #   Form a {Rubirai::FaceMessage}. Only needs to give one of the two arguments.
    #   @option kwargs [Integer] :face_id the face id (high priority)
    #   @option kwargs [String] :name the face's name (low priority)
    #   @return [Rubirai::FaceMessage] the message object
    #   @!scope class
    set_message :Face, :face_id, :name
  end

  # The plain text message type
  class PlainMessage < Message
    # @!attribute [r] text
    #   @return [String] the text
    # @!method from(**kwargs)
    #   @option kwargs [String] :text the plain text
    #   @return [PlainMessage] the message object
    #   @!scope class
    set_message :Plain, :text
  end

  # The image message type.
  # Only one out of the three fields is needed to form the message.
  class ImageMessage < Message
    # @!attribute [r] image_id
    #   @return [String, nil] the image id from mirai
    # @!attribute [r] url
    #   @return [String, nil] the url of the image
    # @!attribute [r] path
    #   @return [String, nil] the local path of the image
    # @!method from(**kwargs)
    #   Form an {Rubirai::ImageMessage}. Only needs to give one of the three arguments.
    #   @option kwargs [String] :image_id the image id
    #   @option kwargs [String] :url the url of the image
    #   @option kwargs [String] :path the local path of the image
    #   @return [Rubirai::ImageMessage] the message object
    #   @!scope class
    set_message :Image, :image_id, :url, :path
  end

  # The flash image message type
  class FlashImageMessage < Message
    # @!attribute [r] image_id
    #   @return [String, nil] the image id from mirai
    # @!attribute [r] url
    #   @return [String, nil] the url of the image
    # @!attribute [r] path
    #   @return [String, nil] the local path of the image
    # @!method from(**kwargs)
    #   Form a {Rubirai::FlashImageMessage}. Only needs to give one of the three arguments.
    #   @option kwargs [String] :image_id the image id
    #   @option kwargs [String] :url the url of the image
    #   @option kwargs [String] :path the local path of the image
    #   @return [Rubirai::FlashImageMessage] the message object
    #   @!scope class
    set_message :FlashImage, :image_id, :url, :path
  end

  # The voice message type
  class VoiceMessage < Message
    # @!attribute [r] voice_id
    #   @return [String, nil] the voice id from mirai
    # @!attribute [r] url
    #   @return [String, nil] the url of the voice
    # @!attribute [r] path
    #   @return [String, nil] the local path of the voice
    # @!method from(**kwargs)
    #   Form a {Rubirai::VoiceMessage}. Only needs to give one of the three arguments.
    #   @option kwargs [String] :voice_id the voice id
    #   @option kwargs [String] :url the url of the voice
    #   @option kwargs [String] :path the local path of the voice
    #   @return [Rubirai::VoiceMessage] the message object
    #   @!scope class
    set_message :Voice, :voice_id, :url, :path
  end

  # The xml message type
  class XmlMessage < Message
    # @!attribute [r] xml
    #   @return [String] the xml content
    # @!method from(**kwargs)
    #   Form a {Rubirai::XmlMessage}.
    #   @option kwargs [String] :xml the xml body
    #   @return [Rubirai::XmlMessage] the message object
    #   @!scope class
    set_message :Xml, :xml
  end

  # The json message type
  class JsonMessage < Message
    # @!attribute [r] json
    #   @return [String] the json content
    # @!method from(**kwargs)
    #   Form a {Rubirai::JsonMessage}.
    #   @option kwargs [String] :json the json body
    #   @return [Rubirai::JsonMessage] the message object
    #   @!scope class
    set_message :Json, :json
  end

  # The app message type
  class AppMessage < Message
    # @!attribute [r] content
    #   @return [String] the app content
    # @!method from(**kwargs)
    #   Form an {Rubirai::AppMessage}.
    #   @option kwargs [String] :content the app body
    #   @return [Rubirai::AppMessage] the message object
    #   @!scope class
    set_message :App, :content
  end

  # The poke message type
  class PokeMessage < Message
    # @!attribute [r] name
    #   @return [String] type (name) of the poke
    # @!method from(**kwargs)
    #   Form an {Rubirai::PokeMessage}.
    #   @option kwargs [String] :name the name (type) of poke
    #   @return [Rubirai::PokeMessage] the message object
    #   @!scope class
    set_message :Poke, :name
  end

  # The forward message type
  class ForwardMessage < Message
    # A message node in the forward message list
    #
    # @!attribute [r] sender_id
    #   @return [Integer] sender id
    # @!attribute [r] time
    #   @return [Integer] send timestamp (second)
    # @!attribute [r] sender_name
    #   @return [String] the sender name
    # @!attribute [r] message_chain
    #   @return [MessageChain] the message chain
    class Node
      attr_reader :sender_id, :time, :sender_name, :message_chain

      # @private
      def initialize(hash, bot = nil)
        return unless hash
        @sender_id = hash['senderId']
        @time = hash['time']
        @sender_name = hash['senderName']
        @message_chain = MessageChain.make(*hash['messageChain'], bot: bot)
      end

      def self.from(**kwargs)
        n = new({})
        %i[sender_id time sender_name message_chain].each do |attr|
          n.instance_variable_set("@#{attr}", kwargs[attr])
        end
      end
    end

    # @!attribute [r] title
    #   @return [String] the title
    # @!attribute [r] brief
    #   @return [String] the brief text
    # @!attribute [r] source
    #   @return [String] the source text
    # @!attribute [r] summary
    #   @return [String] the summary text
    # @!attribute [r] node_list
    #   @return [Array<Node>] the node list
    #   @see Node
    set_message :Forward, :title, :brief, :source, :summary, :node_list

    # @private
    def initialize(hash, bot = nil)
      super :Forward, bot
      @title = hash['title']
      @brief = hash['brief']
      @source = hash['source']
      @summary = hash['summary']
      @node_list = hash['nodeList'].each do |chain|
        Node.new chain, bot
      end
    end
  end

  # The file message type
  class FileMessage < Message
    # @!attribute [r] id
    #   @return [String] the file id
    # @!attribute [r] internal_id
    #   @return [Integer] the internal id needed by server
    # @!attribute [r] name
    #   @return [String] the filename
    # @!attribute [r] size
    #   @return [Integer] the file size
    set_message :File, :id, :internal_id, :name, :size
  end

  # The music share card message
  class MusicShareMessage < Message
    # List all kinds of music providers
    #
    # @return [Array<String>] kinds
    def self.all_kinds
      %w[NeteaseCloudMusic QQMusic MiguMusic]
    end

    # @!attribute [r] kind
    #   @return [String] the kind of music provider
    # @!attribute [r] title
    #   @return [String] the music card title
    # @!attribute [r] summary
    #   @return [String] the music card summary
    # @!attribute [r] jump_url
    #   @return [String] the jump url
    # @!attribute [r] picture_url
    #   @return [String] the picture's url
    # @!attribute [r] music_url
    #   @return [String] the music's url
    # @!attribute [r] brief
    #   @return [String, nil] the brief message (optional)
    set_message :MusicShare, :kind, :title, :summary, :jump_url, :picture_url, :music_url, :brief do |hash|
      raise(RubiraiError, 'non valid music type') unless all_kinds.include? hash['kind']
    end
  end
end