diff --git a/debug.html b/debug.html new file mode 100644 index 0000000..ef20b16 --- /dev/null +++ b/debug.html @@ -0,0 +1,93 @@ + + + + + + Debugger + + + +
+

Welcome to Heimdall Debugger

+
+
+
+
+
+ +
+
+ +
+
+ + + diff --git a/proto.rb b/proto.rb index d87a22b..21dd2bd 100644 --- a/proto.rb +++ b/proto.rb @@ -26,7 +26,7 @@ module Heimdall # @return [self] def new(*args, **params) object = super(*args, **params) - @uuids[object.uuid] = object + @uuids[object.uuid] = WeakRef.new(object) @last = object.uuid add_foreign(object.foreign_ids, object) object @@ -75,7 +75,7 @@ module Heimdall # @return [void] def add_foreign(foreign, obj) foreign.each do |f_id| - @foreign_ids[check_foreign(f_id)] = obj + @foreign_ids[check_foreign(f_id)] = WeakRef.new(obj) end end @@ -137,7 +137,7 @@ module Heimdall # Add a listener to the PubSub # @param listener [#call] # @return [void] - def listen(listener) + def listen(&listener) @listeners.append(listener) end @@ -159,8 +159,16 @@ module Heimdall class Message < UUIDObject def initialize(datahash, **params) @id = datahash["id"] - @from = UUIDObject.get(datahash["from"]) - @to = UUIDObject.get(datahash["to"]) + @from = if datahash["from"] + UUIDObject.get(datahash["from"]) + elsif datahash["from_foreign"] + UUIDObject.get_foreign(datahash["from_foreign"]) + end + @to = if datahash["to"] + UUIDObject.get(datahash["to"]) + elsif datahash["to_foreign"] + UUIDObject.get_foreign(datahash["to_foreign"]) + end @content = datahash["content"] # @reply_to = datahash["reply_to"] # TODO: make this make sense @attachments = datahash["attachments"] @@ -168,6 +176,21 @@ module Heimdall super(**params) end + # Convert message data to a JSON struct + # @return [String] JSON struct + def to_struct + JSON.dump({ + "id" => @id, + "uuid" => @uuid, + "from" => @from.uuid, + "to" => @to.uuid, + "from_foreign" => @from.foreing_ids, + "to_foreign" => @to.foreign_ids, + "content" => @content, + "attachments" => @attachments + }) + end + attr_reader :from, :to, :content, :reply_to, :attachments end @@ -189,6 +212,7 @@ module Heimdall def to_struct JSON.dump({ "id" => @id, + "uuid" => @uuid, "username" => @username, "nickname" => @nickname, "avatar" => @avatar @@ -201,6 +225,7 @@ module Heimdall DEFAULT_AVATAR = "" def initialize(datahash, **params) + @pubsub = PubSub.new @id = datahash["id"] @name = datahash["name"] or @id @avatar = datahash["avatar"] || self.class::DEFAULT_AVATAR @@ -213,10 +238,13 @@ module Heimdall def to_struct JSON.dump({ "id" => @id, + "uuid" => @uuid, "name" => @name, "avatar" => @avatar }) end + + attr_reader :pubsub end VERSION = "1.0" diff --git a/server.ru b/server.ru index 119cbd6..8d0d8bc 100644 --- a/server.ru +++ b/server.ru @@ -3,8 +3,171 @@ require_relative '.env' require_relative 'proto' require 'landline' +require 'landline/extensions/websocket' require 'json' +# Class for handling websocket connection requests +class Reactor + def initialize + @sockets = [] + @events = {} + end + + # Attach websocket to reactor + # @param socket [Landline::WebSocket::WSockWrapper] + def attach(socket) + @sockets.append(socket) + end + + # Push data to all attached sockets + # @param data [Array,Hash] + def push(data) + @sockets.each { |x| x.write(data) } + end + + # Handle websocket event + # @param event [String] + # @param block [#call] + def on(event, &block) + @events[event] ||= [] + @events[event].append(block) + end + + # Main loop + def listen_loop + loop do + readable = IO.select(@sockets, nil, nil, 5)&.fetch(0, nil) + readable&.each do |socket| + output = socket.read + if output + _respond(socket, output) + else + @sockets.delete(socket) + end + end + end + end + + private + + def _respond(socket, output) + data = JSON.parse(output.data) + event_name = data['event'] + @events[event_name]&.each do |callback| + callback.call(socket, + *(data['args'] || []), + **(data['params'] || {})) + end + rescue JSON::ParserError => e + socket.write(JSON.dump({ error: e.message, code: 400 })) + end +end + +# Class acting as session-persistent object storage +class ObjectStorage + def initialize + @channels = {} + @users = {} + end + + # Add channel to object storage + # @param channel [Heimdall::Channel] + def add_channel(channel) + @channels[channel.uuid] = channel + end + + # Add user to object storage + # @param user [Heimdall::User] + def add_user(user) + @users[user.uuid] = user + end + + attr_reader :channels, :users +end + +OBJECT_STORAGE = ObjectStorage.new + +# Helper methods for building reactor listeners +module ReactorHelpers + # Create a block for foreign and uuid indexing of a class + # @param cls [Class] + # @param block [#call] + # @param reactor [Reactor] + def self.foreign_and_uuid(method_name, reactor, cls, &block) + reactor.on "#{cls.name.downcase}.#{method_name}.foreign" do |socket, f_id, + *args, **vars| + obj = cls.get_foreign(f_id) + block.call(socket, obj, *args, **vars) + end + reactor.on "#{cls.name.downcase}.#{method_name}.uuid" do |socket, uuid, + *args, **vars| + obj = cls.get_uuid(uuid) + block.call(socket, obj, *args, **vars) + end + end + + # Boilerplate get/delete functions for a class + # @param reactor [Reactor] + # @param cls [Class] + def self.foreign_and_uuid_boilerplate(reactor, cls) + cname = cls.name.gsub(/\A.+::/, "").downcase + reactor.on "#{cname}.get.foreign" do |socket, f_id| + obj = cls.get_foreign(f_id) + socket.write(obj || JSON.dump({ error: "#{cname} not found", code: 400 })) + end + reactor.on "#{cname}.get.uuid" do |socket, uuid| + obj = cls.get_uuid(uuid) + socket.write(obj || JSON.dump({ error: "#{cname} not found", code: 400 })) + end + reactor.on "#{cname}.delete.foreign" do |socket, f_id| + status = cls.delete_foreign(f_id) + socket.write(JSON.dump(if status + { code: 200 } + else + { error: "#{cname} not found", code: 400 } + end)) + end + reactor.on "#{cname}.delete.uuid" do |socket, f_id| + status = cls.delete_uuid(f_id) + socket.write(JSON.dump(if status + { code: 200 } + else + { error: "#{cname} not found", code: 400 } + end)) + end + end +end + +# WebSocket handling +WEBSOCKET_LISTENER = Reactor.new.tap do |react| + react.on "version" do |socket| + socket.write(JSON.dump({ + "version" => Heimdall::VERSION + })) + end + # --- USER --- + ReactorHelpers.foreign_and_uuid_boilerplate(react, Heimdall::User) + # --- CHANNEL --- + ReactorHelpers.foreign_and_uuid_boilerplate(react, Heimdall::Channel) + ReactorHelpers.foreign_and_uuid("listen", react, + Heimdall::Channel) do |socket, channel| + channel.pubsub.listen do |msg| + socket.write(msg) + end + socket.write(JSON.dump({ code: 200 })) + end + ReactorHelpers.foreign_and_uuid("send", react, + Heimdall::Channel) do |socket, channel, + **message_args| + msg = Heimdall::Message.new(message_args) + channel.pubsub.push(msg.to_struct) + socket.write(JSON.dump({ code: 200, + msg_uuid: msg.uuid })) + end +end + +WEBSOCKET_LISTENER_LOOP = Thread.new { WEBSOCKET_LISTENER.listen_loop } + # Primary server class class HeimdallServer < Landline::App before do @@ -48,6 +211,13 @@ class HeimdallServer < Landline::App }) end + root __dir__ + serve "*.html" + + get "/debug" do + jump "/debug.html" + end + pipeline do |request, &output| output.call(request) rescue Heimdall::ProtoError => e @@ -94,6 +264,7 @@ class HeimdallServer < Landline::App "nickname" => [String, NilClass], "avatar" => [String, NilClass]) new_user = Heimdall::User.new(@data) + OBJECT_STORAGE.add_user(new_user) JSON.dump({ "uuid": new_user.uuid }) end @@ -106,12 +277,17 @@ class HeimdallServer < Landline::App "name" => String, "avatar" => [String, NilClass]) new_channel = Heimdall::Channel.new(@data) + OBJECT_STORAGE.add_channel(new_channel) JSON.dump({ "uuid": new_channel.uuid }) end instance_exec(Heimdall::Channel, &read_delete) end + websocket "/socket" do |socket| + WEBSOCKET_LISTENER.attach(socket) + end + handle do |status, backtrace: nil, **_| backtrace ||= [Landline::Util::HTTP_STATUS[status]] [status,