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,