diff --git a/lib/component.rb b/lib/component.rb index e117008..62d2dd0 100644 --- a/lib/component.rb +++ b/lib/component.rb @@ -1,5 +1,43 @@ module Component - def self.build(&block) - Component::Builder.new(&block).to_component + class << self + def build(&block) + Component::Builder.new(&block).to_component + end + + def from_json(json) + Component::Base.json_create(json) + end + + def from_legacy_text(legacy, **args) + extra = [] + ChatUtils.formatted_spans(legacy) do |text, formats| + c = {} + formats.each do |format| + if format.color? + c[:color] = format.name.downcase + else + case format + when ChatColor::BOLD + c[:bold] = true + when ChatColor::ITALIC + c[:italic] = true + when ChatColor::UNDERLINED + c[:underlined] = true + when ChatColor::STRIKETHROUGH + c[:strikethrough] = true + when ChatColor::OBFUSCATED + c[:obfuscated] = true + end + end + end + extra << Component::Text.new(text, **c) + end + + if extra.size == 1 && args.empty? + extra[0] + else + Component::Text.new('', **args.merge(extra: [*extra, *args[:extra]])) + end + end end end diff --git a/lib/component/base.rb b/lib/component/base.rb index 7c35bf9..ea1bf06 100644 --- a/lib/component/base.rb +++ b/lib/component/base.rb @@ -8,6 +8,21 @@ class << self def assert_flag(flag) FLAGS.include?(flag) or raise ArgumentError, "Unknown formatting flag '#{flag}'" end + + def json_create(json) + if json.is_a?(String) + Component.from_legacy_text(json) + else + args = json_create_args(json) + if text = json['text'] + Component.from_legacy_text(text, **args) + elsif translate = json['translate'] + Translate.new(translate: translate, with: json['with'] || [], **args) + else + new(**args) + end + end + end end def get_flag(flag) @@ -42,10 +57,6 @@ def as_json(*) @json ||= full_json.freeze end - def self.json_create(json) - new(**json_create_args(json)) - end - protected def full_json diff --git a/app/helpers/error_helper.rb b/lib/error_helper.rb similarity index 100% rename from app/helpers/error_helper.rb rename to lib/error_helper.rb diff --git a/lib/ext/ocn/mass_assignment_security.rb b/lib/ext/ocn/mass_assignment_security.rb index 9252675..ce08119 100644 --- a/lib/ext/ocn/mass_assignment_security.rb +++ b/lib/ext/ocn/mass_assignment_security.rb @@ -66,7 +66,7 @@ module DocumentExtensions # This is missing from Mongoid's configuration, so we have to do it this way. # See #ActiveModel::MassAssignmentSecurity::RavenSanitizer included do - self.mass_assignment_sanitizer = if ['production', 'test'].include? Rails.env + self.mass_assignment_sanitizer = if Object::const_defined?(:Raven) && ['production', 'test'].include?(Rails.env) RavenSanitizer.new(self) else :strict @@ -92,7 +92,11 @@ def sanitize_for_mass_assignment(attributes, role = nil) # For whatever reason, Devise tries to mass-assign the email field after a failed login, # and generates lots of pointless errors. This is the best solution I could think of. - if self.is_a?(User) && attributes.key?('email') && !caller.grep(%r[devise/sessions_controller]).empty? + if Object::const_defined?(:Devise) && + self.is_a?(Devise::Models::Confirmable) && + attributes.key?('email') && + !caller.grep(%r[devise/sessions_controller]).empty? + attributes.delete('email') end diff --git a/lib/ext/raven.rb b/lib/ext/raven.rb index fc7af82..bd135ef 100644 --- a/lib/ext/raven.rb +++ b/lib/ext/raven.rb @@ -1,15 +1,17 @@ module Ext module Raven - class HttpInterface < ::Raven::HttpInterface - name 'request' + if Object::const_defined? :Raven + class HttpInterface < ::Raven::HttpInterface + name 'request' - def from_rack(env) - # ActionDispatch::ShowExceptions changes PATH_INFO to the error page - # before it gets here, so we need to change it back. - if original_path = env['action_dispatch.original_path'] - env = env.merge('PATH_INFO' => original_path) + def from_rack(env) + # ActionDispatch::ShowExceptions changes PATH_INFO to the error page + # before it gets here, so we need to change it back. + if original_path = env['action_dispatch.original_path'] + env = env.merge('PATH_INFO' => original_path) + end + super(env) end - super(env) end end @@ -58,7 +60,9 @@ def send_event_async(event) end end -::Raven.extend(::Ext::Raven::ClassMethods) +if Object::const_defined? :Raven + ::Raven.extend(::Ext::Raven::ClassMethods) -# This should replace the one already registered -::Raven.register_interface http: ::Ext::Raven::HttpInterface + # This should replace the one already registered + ::Raven.register_interface http: ::Ext::Raven::HttpInterface +end diff --git a/lib/minecraft/protocol.rb b/lib/minecraft/protocol.rb new file mode 100644 index 0000000..cad649f --- /dev/null +++ b/lib/minecraft/protocol.rb @@ -0,0 +1,111 @@ +require_dependency 'minecraft/protocol/packet' + +module Minecraft + module Protocol + + VERSION = 316 + PROTOCOLS = [:handshaking, :status, :login, :play] + + class ServerInfo + attr :json, + :version, :protocol, + :max_players, :online_players, + :description, :icon, + :map_name, :map_icon, + :participants, :observers + + def decode_icon(uri) + if uri && uri =~ %r{^data:image/png;base64,(.*)$}m + Base64.decode64($1) + end + end + + def initialize(json) + @json = json + if version = json['version'] + @version = version['name'] + @protocol = version['protocol'] + end + if players = json['players'] + @max_players = players['max'] + @online_players = players['online'] + end + @description = json['description'] + if icon = json['favicon'] + @icon = decode_icon(icon) + end + if pgm = json['pgm'] + @participants = pgm['participants'].to_i + @observers = pgm['observers'].to_i + + if map = pgm['map'] + @map_name = map['name'] + @map_icon = decode_icon(map['icon']) + end + end + end + + def pgm? + json.key?('pgm') + end + end + + class Client + def initialize(host:, port: 25565) + @host = host + @port = port + @io = TCPSocket.new(host, port) + @protocol = :handshaking + end + + def read + packet = Packet.read(@io, @protocol, :clientbound) + puts "<<< #{packet.inspect}" + packet + end + + def write(packet) + puts ">>> #{packet.inspect}" + packet.write(@io) + if packet.is_a?(Packet::Handshaking::In::SetProtocol) + @protocol = PROTOCOLS[packet.next_state] + end + end + + def handshake(protocol) + write Packet::Handshaking::In::SetProtocol.new( + protocol_version: VERSION, + server_address: @host, + server_port: @port, + next_state: PROTOCOLS.index(protocol) + ) + end + + def ping(payload = nil) + handshake(:status) + write(Packet::Status::In::Ping.new(payload: payload || Time.now.to_i)) + response = read.payload + @io.close + response + end + + def status + handshake(:status) + write(Packet::Status::In::Start.new) + json = JSON.parse(read.json) + @io.close + ServerInfo.new(json) + end + end + + class << self + def ping(host:, port: 25565, payload: nil) + Client.new(host: host, port: port).ping(payload) + end + + def status(host:, port: 25565) + Client.new(host: host, port: port).status + end + end + end +end diff --git a/lib/minecraft/protocol/data.rb b/lib/minecraft/protocol/data.rb new file mode 100644 index 0000000..9dea7c0 --- /dev/null +++ b/lib/minecraft/protocol/data.rb @@ -0,0 +1,108 @@ +module Minecraft + module Protocol + class Transcoder + def values + @values ||= [] + end + + def initialize(io) + @io = io + end + + def pack(length, format) + raise NoMethodError + end + + def byte + pack(1, 'c') + end + + def ubyte + pack(1, 'C') + end + + def short + pack(2, 's>') + end + + def ushort + pack(2, 'S>') + end + + def integer + pack(4, 'i>') + end + + def long + pack(8, 'q>') + end + + def float + pack(4, 'g') + end + + def double + pack(8, 'G') + end + end + + class Decoder < Transcoder + def pack(length, format) + values << @io.read(length).unpack(format)[0] + end + + def varnum(len) + n = v = 0 + loop do + b = @io.read(1).ord + v |= (b & 0x7f) << (7 * n) + break if b & 0x80 == 0 + n += 1 + raise "VarInt too long" if n > len + end + values << v + end + + def varint + varnum(5) + end + + def varlong + varnum(10) + end + + def string + varint + values << @io.read(values.pop) + end + end + + class Encoder < Transcoder + def pack(length, format) + @io.write([values.shift].pack(format)) + end + + def varint + v = values.shift % 0x1_0000_0000 + loop do + b = v & 0x7f + v >>= 7 + b |= 0x80 unless v == 0 + @io.putc(b) + break if v == 0 + end + end + + def varlong + varint + end + + def string + v = values.shift + values.unshift(v.size) + varint + @io.write(v) + end + end + end +end diff --git a/lib/minecraft/protocol/packet.rb b/lib/minecraft/protocol/packet.rb new file mode 100644 index 0000000..1f377a0 --- /dev/null +++ b/lib/minecraft/protocol/packet.rb @@ -0,0 +1,111 @@ +require_dependency 'minecraft/protocol/data' + +module Minecraft + module Protocol + module Packet + module Serverbound + def direction + :serverbound + end + end + + module Clientbound + def direction + :clientbound + end + end + + class << self + def packets + # protocol -> direction -> packet_id -> class + @packets ||= Hash.default{ Hash.default{ {} } } + end + + def read(io, protocol, direction) + decoder = Decoder.new(io) + decoder.varint # length + decoder.varint # ID + packet_id = decoder.values[1] + + unless cls = Packet.packets[protocol.to_sym][direction.to_sym][packet_id] + raise "Unknown packet #{protocol}:#{direction}:#{packet_id}" + end + + decoder.values.clear + cls.transcode_fields(decoder) + cls.new(*decoder.values) + end + end + + class Base + class << self + attr :packet_id + + def id(packet_id) + @packet_id = packet_id + Packet.packets[protocol][direction][packet_id] = self + end + + def inspect + "#{protocol}:#{direction}:#{base_name}(#{packet_id})" + end + + def fields + @fields ||= {} + end + + def field(name, type) + index = fields.size + fields[name.to_sym] = type.to_sym + + define_method name do + @values[index] + end + + define_method "#{name}=" do |value| + @values[index] = value + end + end + + def transcode_fields(stream) + fields.values.each do |type| + stream.__send__(type) + end + end + end + + def initialize(*values, **fields) + @values = values + self.class.fields.each do |name, _| + @values << fields[name] + end + end + + def inspect + "#{self.class.inspect}{#{self.class.fields.map{|name, _| "#{name}=#{__send__(name).inspect}"}.join(' ')}}" + end + + def write(io) + io.write(encode) + end + + def encode + encoded = "" + encoder = Encoder.new(StringIO.new(encoded)) + encoder.values << self.class.packet_id + encoder.values.concat(@values) + + encoder.varint # packet_id + self.class.transcode_fields(encoder) + + prefix = "" + encoder = Encoder.new(StringIO.new(prefix)) + encoder.values << encoded.size + encoder.varint # length + + prefix + encoded + end + end + end + end +end diff --git a/lib/minecraft/protocol/packet/handshaking.rb b/lib/minecraft/protocol/packet/handshaking.rb new file mode 100644 index 0000000..7c90fbc --- /dev/null +++ b/lib/minecraft/protocol/packet/handshaking.rb @@ -0,0 +1,35 @@ +require_dependency 'minecraft/protocol/packet' + +module Minecraft + module Protocol + module Packet + module Handshaking + class Base < Packet::Base + def self.protocol + :handshaking + end + end + + module In + class Base < Handshaking::Base + extend Serverbound + end + + class SetProtocol < Base + id 0 + field :protocol_version, :varint + field :server_address, :string + field :server_port, :ushort + field :next_state, :varint + end + end + + module Out + class Base < Handshaking::Base + extend Clientbound + end + end + end + end + end +end diff --git a/lib/minecraft/protocol/packet/status.rb b/lib/minecraft/protocol/packet/status.rb new file mode 100644 index 0000000..9f24149 --- /dev/null +++ b/lib/minecraft/protocol/packet/status.rb @@ -0,0 +1,46 @@ +require_dependency 'minecraft/protocol/packet' + +module Minecraft + module Protocol + module Packet + module Status + class Base < Packet::Base + def self.protocol + :status + end + end + + module In + class Base < Status::Base + extend Serverbound + end + + class Start < Base + id 0 + end + + class Ping < Base + id 1 + field :payload, :long + end + end + + module Out + class Base < Status::Base + extend Clientbound + end + + class ServerInfo < Base + id 0 + field :json, :string + end + + class Pong < Base + id 1 + field :payload, :long + end + end + end + end + end +end diff --git a/app/helpers/params_helper.rb b/lib/params_helper.rb similarity index 100% rename from app/helpers/params_helper.rb rename to lib/params_helper.rb