diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84fd7ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.env.rb +/hyde/ diff --git a/proto.rb b/proto.rb index 456127a..d87a22b 100644 --- a/proto.rb +++ b/proto.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true +require 'weakref' + module Heimdall + # Protocol error class + class ProtoError < StandardError + end + # Container for any uniquely identifiable object # @abstract class UUIDObject @@ -18,43 +24,92 @@ module Heimdall # Create a new UUIDObject with unique UUID # @return [self] - def new - object = super + def new(*args, **params) + object = super(*args, **params) @uuids[object.uuid] = object @last = object.uuid - @foreign_ids[object.foreign_id] = object if object.foreign_id + add_foreign(object.foreign_ids, object) object end + # Delete an object found by UUID + # @param uuid [Integer] + # @return [void] + def delete(uuid) + obj = @uuids[uuid] + return false unless obj + + @uuids.delete(obj.uuid) + obj.foreign_ids.each { |x| @foreign_ids.delete(x) } + true + end + + # Delete an object found by foreign id + # @param foreign_id [String] + # @return [void] + def delete_foreign(foreign_id) + obj = @foreign_ids[foreign_id] + return false unless obj + + @uuids.delete(obj.uuid) + obj.foreign_ids.each { |x| @foreign_ids.delete(x) } + true + end + # Get object by UUID - # @param uuid + # @param uuid [Integer] # @return [self, nil] def get(uuid) @uuids[uuid] if @uuids[uuid].is_a? self end # Get object by foreign id - # @param foreign_id + # @param foreign_id [String] # @return [self, nil] def get_foreign(foreign_id) @foreign_ids[foreign_id] if @foreign_ids[foreign_id].is_a? self end + # Add foreign ids to the list of known foreign ids + # @param foreign [Array] + # @return [void] + def add_foreign(foreign, obj) + foreign.each do |f_id| + @foreign_ids[check_foreign(f_id)] = obj + end + end + + # Check a foreign id for valid syntax + # @param foreign [String] + # @return [String] + def check_foreign(foreign) + if foreign_ids.include?(foreign) + raise ProtoError, 'foreign id already exists' + end + + unless foreign.match?(/^([\w_]+):.*$/) + raise ProtoError, 'invalid foreign id syntax' + end + + foreign + end + attr_accessor :uuids, :foreign_ids, :last end def initialize @uuid = __gen_uuid @foreign_ids = [] + @foreign_ids.append(@id) if @id end - attr_reader :uuid, :foreign_id + attr_reader :uuid, :foreign_ids private def __gen_uuid newuuid = (Time.now.to_f * 1000).to_i * 10000 - if (self.class.last / 1000) == (newuuid / 1000) + if self.class.last && (self.class.last / 1000) == (newuuid / 1000) newuuid += (self.class.last % 1000) + 1 end newuuid @@ -103,13 +158,14 @@ module Heimdall # Message struct class Message < UUIDObject def initialize(datahash, **params) - super(**params) - + @id = datahash["id"] @from = UUIDObject.get(datahash["from"]) @to = UUIDObject.get(datahash["to"]) @content = datahash["content"] # @reply_to = datahash["reply_to"] # TODO: make this make sense @attachments = datahash["attachments"] + + super(**params) end attr_reader :from, :to, :content, :reply_to, :attachments @@ -117,8 +173,51 @@ module Heimdall # User struct class User < UUIDObject - def initialize(datahash, **params) - super(**params) + DEFAULT_AVATAR = "" + def initialize(datahash, **params) @id = datahash["id"] + @username = datahash["username"] + @nickname = datahash["nickname"] + @avatar = datahash["avatar"] || self.class::DEFAULT_AVATAR + + super(**params) + end + + # Convert user data to a JSON struct + # @return [String] JSON struct + def to_struct + JSON.dump({ + "id" => @id, + "username" => @username, + "nickname" => @nickname, + "avatar" => @avatar + }) + end + end + + # Channel struct + class Channel < UUIDObject + DEFAULT_AVATAR = "" + + def initialize(datahash, **params) + @id = datahash["id"] + @name = datahash["name"] or @id + @avatar = datahash["avatar"] || self.class::DEFAULT_AVATAR + + super(**params) + end + + # Convert channel data to a JSON struct + # @return [String] JSON struct + def to_struct + JSON.dump({ + "id" => @id, + "name" => @name, + "avatar" => @avatar + }) + end + end + + VERSION = "1.0" end diff --git a/server.ru b/server.ru index c5ca920..119cbd6 100644 --- a/server.ru +++ b/server.ru @@ -8,6 +8,8 @@ require 'json' # Primary server class class HeimdallServer < Landline::App before do + header "content-type", "application/json" + # Match data type against a list of datatypes # @param obj [Object] # @param type [Array, Class] @@ -23,41 +25,103 @@ class HeimdallServer < Landline::App # @param args [Hash] hash of key - type pairs to check JSON data against def validate_json(**args) die(400, backtrace: ['JSON body expected']) unless json? - data = begin + @data = begin JSON.parse(request.body) rescue StandardError die(400, backtrace: ['JSON body is invalid']) end args.each do |k, v| - unless data.include?(k) and match_type(data[k], v) - die(400, backtrace: ["Key #{k} is missing"]) + unless match_type(@data[k], v) + die(400, backtrace: ["Key #{k} of type #{v} is missing"]) end end end end filter do - request.cookies["token"] == BOT_TOKEN + defined? ::BOT_TOKEN ? request.cookies["token"] == ::BOT_TOKEN : true + end + + get "/version" do + JSON.dump({ + "version" => Heimdall::VERSION + }) + end + + pipeline do |request, &output| + output.call(request) + rescue Heimdall::ProtoError => e + throw :finish, [400, + { "content-type": "application/json" }, + JSON.dump({ + "error" => e.message, + "code" => 400 + })] + end + + read_delete = proc do |cls| + get "/foreign/*" do |id| + user = cls.get_foreign(id) + user&.to_struct or die(400, backtrace: ["#{cls} not found"]) + end + + get "/uuid/*" do |id| + user = cls.get(id.to_i) + user&.to_struct or die(400, backtrace: ["#{cls} not found"]) + end + + delete "/foreign/*" do |id| + if cls.delete_foreign(id) + JSON.dump({ "code" => 200 }) + else + die(400, backtrace: ["#{cls} not found"]) + end + end + + delete "/uuid/*" do |id| + if cls.delete(id.to_i) + JSON.dump({ "code" => 200 }) + else + die(400, backtrace: ["#{cls} not found"]) + end + end end path "/user" do post "/register" do - validate_json("id" => Integer, + validate_json("id" => String, "username" => String, - "nickname" => [String, NilClass]) - + "nickname" => [String, NilClass], + "avatar" => [String, NilClass]) + new_user = Heimdall::User.new(@data) + JSON.dump({ "uuid": new_user.uuid }) end + + instance_exec(Heimdall::User, &read_delete) end - handle do |status, backtrace: nil| - page = JSON.dump({ - "error" => backtrace.join("\n"), - "code" => status - }) - [{ - "content-length": page.bytesize, - "content-type": "application/json" - }, page] + path "/channel" do + post "/register" do + validate_json("id" => String, + "name" => String, + "avatar" => [String, NilClass]) + new_channel = Heimdall::Channel.new(@data) + JSON.dump({ "uuid": new_channel.uuid }) + end + + instance_exec(Heimdall::Channel, &read_delete) + end + + handle do |status, backtrace: nil, **_| + backtrace ||= [Landline::Util::HTTP_STATUS[status]] + [status, + { + "content-type": "application/json" + }, + JSON.dump({ + "error" => (backtrace || []).join("\n"), + "code" => status + })] end end