require "kemal" require "uuid" require "file_utils" require "authd" class Task JSON.mapping({ id: String, author: Int32, title: String, description: String, column: String, assigned_to: Int32? }) def initialize(@title, @author, @description, @column) @id = UUID.random.to_s end end class Column JSON.mapping({ id: String, name: String }) def initialize(@name) @id = UUID.random.to_s end end class Project JSON.mapping({ id: String, name: String, columns: Array(Column), tasks: Array(Task), color: { type: String, default: "dark" } }) def initialize(@name) @id = UUID.random.to_s @columns = [Column.new "To do"] @tasks = [] of Task @color = "dark" write! end def write! Dir.mkdir_p "./projects/#{@id}" File.write "./projects/#{@id}/project.json.new", to_json FileUtils.mv "./projects/#{@id}/project.json.new", "./projects/#{@id}/project.json" end # FIXME: Transform that exception somehow. # FIXME: Update to not read everything. def self.get_from_id(id) self.all.find(&.id.==(id)).not_nil! end def self.all : Array(Project) # FIXME: switch to an index file per project in a storage directory. Dir.children("./projects").compact_map do |filename| begin Project.from_json File.read "./projects/#{filename}/project.json" rescue end end rescue [] of Project end end class Requests::Login JSON.mapping({ type: String, login: String, password: String }) end class Requests::Tasks JSON.mapping({ type: String, project: String }) end class Requests::NewProject JSON.mapping({ type: String, name: String }) end class Requests::EditProject JSON.mapping({ type: String, project: String, name: String?, color: String? }) end class Requests::NewColumn JSON.mapping({ type: String, project: String, name: String }) end class Requests::NewTask JSON.mapping({ type: String, project: String, column: String, title: String, description: String }) end class Requests::EditTask JSON.mapping({ type: String, project: String, task: String, column: String?, title: String?, description: String?, assigned_to: Int32? }) end class Requests::DeleteTask JSON.mapping({ type: String, project: String, task: String }) end class Requests::EditColumn JSON.mapping({ type: String, project: String, column: String, name: String? }) end class Requests::DeleteColumn JSON.mapping({ type: String, project: String, column: String }) end class Requests::GetUser JSON.mapping({ type: String, uid: Int32 }) end authd = AuthD::Client.new Kemal.config.extra_options do |parser| parser.on "-k file", "--jwt-key file", "Provides the JWT key for authd." do |file| authd.key = File.read(file).chomp end end sockets = [] of HTTP::WebSocket users = {} of HTTP::WebSocket => AuthD::User ws "/socket" do |socket| socket.on_close do users.delete socket sockets.delete socket end socket.on_message do |message_as_s| message = JSON.parse(message_as_s).as_h case message["type"] when "login" request = Requests::Login.from_json message_as_s login = request.login password = request.password user = authd.get_user? login, password unless user socket.send({ type: "login-error", error: "Invalid credentials." }.to_json) next end users[socket] = user sockets << socket socket.send({ type: "login" }.to_json) socket.send({ type: "list-projects", projects: Project.all }.to_json) when "new-project" user = users[socket] # FIXME: make it an authentication error request = Requests::NewProject.from_json message_as_s project = Project.new request.name # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "project" user = users[socket] # FIXME: make it an authentication error request = Requests::Tasks.from_json message_as_s project = Project.get_from_id request.project socket.send({ type: "project", project: project }.to_json) when "edit-project" user = users[socket] # FIXME: make it an authentication error request = Requests::EditProject.from_json message_as_s project = Project.get_from_id request.project if name = request.name project.name = name end if color = request.color project.color = color end project.write! # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "new-column" user = users[socket] # FIXME: make it an authentication error request = Requests::NewColumn.from_json message_as_s project = Project.get_from_id request.project project.columns << Column.new request.name project.write! # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "new-task" user = users[socket] # FIXME: make it an authentication error request = Requests::NewTask.from_json message_as_s project = Project.get_from_id request.project column = project.columns.find(&.id.==(request.column)).not_nil! project.tasks << Task.new request.title, user.uid, request.description, column.id project.write! # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "edit-task" user = users[socket] # FIXME: make it an authentication error request = Requests::EditTask.from_json message_as_s project = Project.get_from_id request.project task = project.tasks.find(&.id.==(request.task)).not_nil! if column = request.column column = project.columns.find(&.id.==(column)).not_nil! task.column = column.id end if title = request.title task.title = title end if description = request.description task.description = description end # FIXME: Check it’s a valid UID. if assigned_to = request.assigned_to # FIXME: Probably not the best way to handle this corner-case. assigned_to = nil if assigned_to == -1 task.assigned_to = assigned_to end project.write! # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "delete-task" user = users[socket] request = Requests::DeleteTask.from_json message_as_s project = Project.get_from_id request.project project.tasks.select! &.id.!=(request.task) project.write! # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "edit-column" user = users[socket] # FIXME: make it an authentication error request = Requests::EditColumn.from_json message_as_s project = Project.get_from_id request.project column = project.columns.find(&.id.==(request.column)).not_nil! if name = request.name column.name = name end puts project.columns.to_json project.write! # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "delete-column" user = users[socket] request = Requests::DeleteColumn.from_json message_as_s project = Project.get_from_id request.project project.columns.select! &.id.!=(request.column) project.tasks.select! &.column.!=(request.column) project.write! # FIXME: Only notify concerned users. sockets.each &.send({ type: "project", project: project }.to_json) when "get-user" request = Requests::GetUser.from_json message_as_s user = authd.get_user? request.uid socket.send({ type: "user", user: user }.to_json) end end end Kemal.run