Skip to content
This repository has been archived by the owner on Jun 11, 2023. It is now read-only.

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
renatomassaro committed Feb 11, 2018
1 parent 10b773f commit 95dea85
Show file tree
Hide file tree
Showing 31 changed files with 1,010 additions and 174 deletions.
2 changes: 2 additions & 0 deletions lib/event/dispatcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ defmodule Helix.Event.Dispatcher do
event SoftwareEvent.Cracker.Bruteforce.Processed
event SoftwareEvent.Cracker.Overflow.Processed
event SoftwareEvent.File.Added
event SoftwareEvent.File.Deleted
event SoftwareEvent.File.Downloaded
event SoftwareEvent.File.DownloadFailed
event SoftwareEvent.File.Install.Processed
Expand Down Expand Up @@ -235,6 +236,7 @@ defmodule Helix.Event.Dispatcher do
event StoryEvent.Reply.Sent
event StoryEvent.Step.ActionRequested
event StoryEvent.Step.Proceeded
event StoryEvent.Step.Restarted

# Custom handlers
event StoryEvent.Reply.Sent,
Expand Down
4 changes: 2 additions & 2 deletions lib/hell/hack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ defmodule HELL.Hack.Experience do
{:render, 3}
],
"Elixir.Helix.Story.Model.Steppable" => [
{:start, 2},
{:setup, 2},
{:start, 1},
{:setup, 1},
{:handle_event, 3},
{:complete, 1},
{:restart, 3},
Expand Down
50 changes: 50 additions & 0 deletions lib/software/event/file.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,56 @@ defmodule Helix.Software.Event.File do
end
end

event Deleted do
@moduledoc """
FileDeletedEvent is fired when a file has been deleted on the filesystem.
Most of the times is called as a result of FileDeleteProcessedEvent
"""

alias Helix.Server.Model.Server
alias Helix.Software.Model.File

@type t ::
%__MODULE__{
file_id: File.id,
server_id: Server.id
}

event_struct [:file_id, :server_id]

@spec new(File.id, Server.id) ::
t
def new(file_id = %File.ID{}, server_id = %Server.ID{}) do
%__MODULE__{
file_id: file_id,
server_id: server_id
}
end

notify do
@moduledoc """
Pushes the notification to the Client, so it can remove the deleted file.
"""

@event :file_deleted

def generate_payload(event, _socket) do
data = %{
file_id: to_string(event.file_id)
}

{:ok, data}
end

def whom_to_notify(event),
do: %{server: [event.server_id]}
end

listenable(event) do
[event.file_id]
end
end

event Downloaded do
@moduledoc """
FileDownloadedEvent is fired when a FileTransfer process of type `download`
Expand Down
1 change: 1 addition & 0 deletions lib/software/event/handler/filesystem.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule Helix.Software.Event.Handler.Filesystem do
# Existing entries being updated

# Existing entries being removed
# TODO FileDeletedEvent

# Generic notifiers

Expand Down
7 changes: 5 additions & 2 deletions lib/software/make/file.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ defmodule Helix.Software.Make.File do
@typep file_parent :: Server.t | Storage.id
@typep version :: File.Module.version

@typep file_return(type) ::
{:ok, File.t_of_type(type), %{}, [FileAddedEvent.t]}

@doc """
Generates a cracker.
"""
@spec cracker(file_parent, cracker_modules, data) ::
{:ok, File.t_of_type(:cracker), %{}, []}
file_return(:cracker)
def cracker(parent, modules, data \\ %{}),
do: file(parent, :cracker, modules, data)

Expand All @@ -34,7 +37,7 @@ defmodule Helix.Software.Make.File do
end

@spec file(file_parent, Software.type, modules, data) ::
{:ok, File.t, %{}, []}
file_return(Software.type)
defp file(server = %Server{}, type, modules, data) do
server
|> CacheQuery.from_server_get_storages!()
Expand Down
2 changes: 1 addition & 1 deletion lib/software/model/public_ftp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ defmodule Helix.Software.Model.PublicFTP do
@doc """
Verifies whether the given server is active (enabled) or not.
"""
def is_active?(pftp = %__MODULE__{is_active: is_active?}),
def is_active?(%__MODULE__{is_active: is_active?}),
do: is_active?

@spec create_changeset(creation_params) ::
Expand Down
2 changes: 1 addition & 1 deletion lib/story/action/flow/story.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule Helix.Story.Action.Flow.Story do
with \
{:ok, _} <- ContextFlow.setup(entity),
{:ok, story_step} <- StoryAction.proceed_step(first_step),
{:ok, _, events} <- Steppable.start(first_step, nil),
{:ok, _, events} <- Steppable.start(first_step),
on_success(fn -> Event.emit(events, from: relay) end)
do
{:ok, story_step}
Expand Down
44 changes: 44 additions & 0 deletions lib/story/action/story.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Helix.Story.Action.Story do
alias Helix.Story.Event.Email.Sent, as: EmailSentEvent
alias Helix.Story.Event.Reply.Sent, as: ReplySentEvent
alias Helix.Story.Event.Step.Proceeded, as: StepProceededEvent
alias Helix.Story.Event.Step.Restarted, as: StepRestartedEvent

@spec proceed_step(first_step :: Step.t) ::
{:ok, Story.Step.t}
Expand Down Expand Up @@ -64,6 +65,15 @@ defmodule Helix.Story.Action.Story do
def notify_step(prev_step, next_step),
do: [StepProceededEvent.new(prev_step, next_step)]

@spec notify_restart(Step.t, atom, Step.email_id, Step.email_meta) ::
[StepRestartedEvent.t]
@doc """
Generates the StepRestartedEvent, used to notify the client that the step has
been restarted
"""
def notify_restart(step, reason, checkpoint, meta),
do: [StepRestartedEvent.new(step, reason, checkpoint, meta)]

@spec send_email(Step.t, Step.email_id, Step.email_meta) ::
{:ok, [EmailSentEvent.t]}
| {:error, :internal}
Expand Down Expand Up @@ -122,4 +132,38 @@ defmodule Helix.Story.Action.Story do
end
end)
end

@spec rollback_emails(Step.t, Step.email_id, Step.email_meta) ::
{:ok, Story.Step.t, Story.Email.t}
| {:error, :internal}
@doc """
Rollbacks the messages on `step` to the specified `checkpoint`.
Note that within the Story domain, messages are saved on two places:
- within Story.Step, used for internal step stuff (handling replies, etc)
- within Story.Email, used for listing messages per contact (with metadata)
As such, we need to update (rollback) to the checkpoint on both places.
The `allowed_replies` list, specified at `Story.Step.t`, will also be updated
with the default (unlocked) replies listed on `checkpoint` declaration.
"""
def rollback_emails(step, checkpoint, meta) do
result =
Repo.transaction fn ->
with \
{:ok, story_step} <- StepInternal.rollback_email(step, checkpoint),
{:ok, email} <- EmailInternal.rollback_email(step, checkpoint, meta)
do
{story_step, email}
else
_ ->
Repo.rollback(:internal)
end
end

with {:ok, {story_step, story_email}} <- result do
{:ok, story_step, story_email}
end
end
end
72 changes: 37 additions & 35 deletions lib/story/event/handler/story.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Helix.Story.Event.Handler.Story do
Once an event is received, we figure out the entity responsible for that event
and verify whether the StepFlow should be followed. The StepFlow guides the
Step through the Steppable protocol, allow it to react to the event, either
by ignoring it, completing the step or failing it.
by ignoring it, completing the step or restarting it.
"""

import HELF.Flow
Expand All @@ -30,7 +30,7 @@ defmodule Helix.Story.Event.Handler.Story do
Emits:
- Events returned by `Steppable` methods
- StepProceededEvent.t when action is :complete
- StepFailedEvent.t, StepRestartedEvent.t when action is :restart
- StepRestartedEvent.t when action is :restart
"""
def event_handler(event) do
with \
Expand All @@ -45,6 +45,10 @@ defmodule Helix.Story.Event.Handler.Story do
end
end

@doc """
Handler for `StepActionRequestedEvent`, directly relaying the requested action
to the corresponding handler at `handle_action/2`.
"""
def action_handler(event = %StepActionRequestedEvent{}) do
with \
%{object: step} <-
Expand All @@ -63,7 +67,7 @@ defmodule Helix.Story.Event.Handler.Story do
its documentation for more information.
Once the event is handled by the step, the returned action is handled by
StepFlow. It may be one of `:complete | :fail | :noop`. See doc on
StepFlow. It may be one of `:complete | {:restart, _, _} | :noop`. See doc on
`handle_action/2`.
"""
defp step_flow(step) do
Expand Down Expand Up @@ -100,16 +104,17 @@ defmodule Helix.Story.Event.Handler.Story do
end

docp """
If the request is to fail/abort an step, we'll call `Steppable.fail/1`,
and then handle the failure with `fail_step/1`
If the request is to restart a step, we'll call `Steppable.restart/3`, and
then handle the restart with `restart_step/1`.
"""
defp handle_action({:restart, reason, checkpoint}, step) do
with {:ok, step, events} <- Steppable.restart(step, reason, checkpoint) do
Event.emit(events, from: step.event)
with \
{:ok, step, meta, events} <- Steppable.restart(step, reason, checkpoint),
Event.emit(events, from: step.event),

hespawn fn ->
fail_step(step)
end
:ok <- restart_step(step, reason, checkpoint, meta)
do
:ok
end
end

Expand Down Expand Up @@ -138,7 +143,7 @@ defmodule Helix.Story.Event.Handler.Story do
# /\ Proceeds player to the next step

# Generate next step data/meta
{:ok, next_step, events} <- Steppable.start(next_step, prev_step),
{:ok, next_step, events} <- Steppable.start(next_step),
Event.emit(events, from: prev_step.event),

# Update step meta
Expand All @@ -153,31 +158,28 @@ defmodule Helix.Story.Event.Handler.Story do
end

docp """
See comments & implement me.
Updates the database, so the step restart is persisted.
Emits: StepFailedEvent.t, StepRestartedEvent.t
It will:
- update the DB with the new step metadata
- rollback the emails to the specified checkpoint
- notify the client that the step has been restarted
Emits: StepRestartedEvent.t
"""
defp fail_step(_step) do
# Default fail_step implementation is TODO.
# Possible implementation:
# 1 - Remove all emails/replies sent through that step
# 2 - Undo/delete all objects generated on `Steppable.setup`*
# 3 - Call `Steppable.setup`, effectively restarting the step.
#
# Possible problems:
# 1 - Email/reply ids are not unique across steps, so step 1 should take
# this into consideration.
# /\ - add counter of emails sent during the current step
#
# 2 - UX: If mission is reset right after it's failed, the client may
# receive the `stepproceeded**` event almost at the same time as
# `stepfailed` event, so user experience should be considered
# /\ - see note; use `StepRestartedEvent`
#
# Notes:
# * - This should be done at `Steppable.fail`
# ** - In fact, mission "resetup" should be a different event, maybe
# `StepRestarted`. Otherwise, the client would get `StepProceeded` after
# the step has failed, which doesn't quite make sense.
defp restart_step(step, reason, checkpoint, meta) do
with \
{:ok, _} <- StoryAction.update_step_meta(step),
# /\ Make sure the step metadata is updated on the DB

# Rollback to the specified checkpoint
{:ok, _, _} <- StoryAction.rollback_emails(step, checkpoint, meta),

# Notify about step restart
event = StoryAction.notify_restart(step, reason, checkpoint, meta),
Event.emit(event, from: step.event)
do
:ok
end
end
end
64 changes: 64 additions & 0 deletions lib/story/event/step.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,70 @@ defmodule Helix.Story.Event.Step do
end
end

event Restarted do
@moduledoc """
Story.StepRestarted is fired when the step progress has been restarted due
to some `reason`.
"""

alias Helix.Entity.Model.Entity
alias Helix.Story.Model.Step

@type t ::
%__MODULE__{
entity_id: Entity.id,
step: Step.t,
reason: atom,
checkpoint: Step.email_id,
meta: Step.email_meta,
}

event_struct [:entity_id, :step, :reason, :checkpoint, :meta]

@spec new(Step.t, atom, Step.email_id, Step.email_meta) ::
t
def new(step = %_{entity_id: _}, reason, checkpoint, meta) do
%__MODULE__{
entity_id: step.entity_id,
step: step,
reason: reason,
checkpoint: checkpoint,
meta: meta
}
end

notify do
@moduledoc false

alias HELL.Utils

@event :story_step_restarted

def generate_payload(event, _socket) do
allowed_replies =
event.step
|> Step.get_replies(event.checkpoint)
|> Enum.map(&to_string/1)

data = %{
step: to_string(event.step.name),
reason: to_string(event.reason),
checkpoint: event.checkpoint,
meta: Utils.stringify_map(event.meta),
allowed_replies: allowed_replies
}

{:ok, data}
end

@doc """
Notifies only the player
"""
def whom_to_notify(event),
do: %{account: event.entity_id}
end
end

event ActionRequested do
@moduledoc """
`StepActionRequestedEvent` is fired when a callback, declared at the Step
Expand Down
Loading

0 comments on commit 95dea85

Please sign in to comment.