Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 100 additions & 13 deletions lib/alice/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,87 @@ defmodule Alice.Adapter do
`Alice.Message` struct and call `Alice.Bot.handle_message(bot, msg)`.
"""

@type bot :: pid
@type state :: term
@type opts :: any
@type msg :: Alice.Message.t
@typedoc "The pid of the bot process"
@type bot :: pid

@callback reply(bot, msg) :: term
@typedoc "The state for the adapter. Normally a Map, but can be anything"
@type state :: any

@doc false
@typedoc "Initialization options for the adapter"
@type options :: keyword

@typedoc "An Alice.Message struct"
@type msg :: Alice.Message.t

@doc """
Invoked when the adapter starts. Use this callback to connect to
your service and initialize any state that the adapter needs to
keep track of.

In case of successful start, this callback must return a tuple
where the first element is `:ok` and the second is the state.

Returning `{:stop, reason}` will cause `start_link/3` to return
`{:error, reason}` and the process to exit with reason `reason`
without entering the loop or calling `terminate/2`.
"""
@callback connect(bot, options) ::
{:ok, state} |
{:stop, reason :: any}

@doc """
This is an optional callback that may be implemented if you need to
do any work after a successful connection.
"""
@callback handle_connected(state) :: {:ok, state}

@doc """
This callback is required and handles outgoing messages from the bot
process to your service. This is the appropriate place to translate
the `Alice.Message` struct into a message format that your service
understands and send the message to the external service.
"""
@callback handle_outgoing(msg, state) :: {:ok, state}

@doc """
This callback is required and handles incoming messages from your
external service to a bot instance. This is the appropriate place
to translate a message from your service into an `Alice.Message`
struct.

Returning `{:ok, msg, state}` will send the message to the bot
instance to be dispatched to handlers.

Returning `{:noreply, state}` will effectively ignore the message
and nothing will be dispatched to the handlers.
"""
@callback handle_incoming(ext_msg :: any, state) ::
{:ok, msg, state} |
{:noreply, state}

@doc "start an adapter"
def start_link(adapter_module, bot_pid, opts) do
GenServer.start_link(adapter_module, {bot_pid, opts})
end

@doc "send an outgoing message through an adapter"
def reply(adapter_pid, %Alice.Message{} = msg) do
GenServer.cast(adapter_pid, {:outgoing, msg})
end

@doc false
defmacro __using__(_opts) do
quote do
use GenServer
@behaviour Alice.Adapter

def reply(pid, %Alice.Message{} = msg) do
GenServer.cast(pid, {:reply, msg})
end
@doc "Default implementation for `handle_connected`"
def handle_connected(state), do: {:ok, state}

@doc false
def stop(bot, timeout \\ 5000) do
ref = Process.monitor(bot)
Process.exit(bot, :normal)
def stop(adapter, timeout \\ 5000) do
ref = Process.monitor(adapter)
Process.exit(adapter, :normal)
receive do
{:DOWN, ^ref, _, _, _} -> :ok
after
Expand All @@ -41,7 +96,39 @@ defmodule Alice.Adapter do
:ok
end

defoverridable [reply: 2]
@doc false
def init({bot, opts}) do
with {:ok, state} <- connect(bot, opts) do
send(self(), :connected)
{:ok, {bot, state}}
else
{:exit, reason} -> {:exit, reason}
end
end

@doc false
def handle_cast({:outgoing, %Alice.Message{} = msg}, {bot, state}) do
{:ok, state} = handle_outgoing(msg, state)
{:noreply, {bot, state}}
end

@doc false
def handle_info(:connected, {bot, state}) do
:ok = Alice.Bot.handle_connect(bot)
{:ok, state} = handle_connected(state)
{:noreply, {bot, state}}
end
def handle_info({:incoming, ext_msg}, {bot, state}) do
state = case handle_incoming(ext_msg, state) do
{:ok, %Alice.Message{} = msg, state} ->
Alice.Bot.handle_in(bot, msg)
state
{:noreply, state} -> state
end
{:noreply, {bot, state}}
end

defoverridable handle_connected: 1
end
end
end
22 changes: 7 additions & 15 deletions lib/alice/adapters/console.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,19 @@ defmodule Alice.Adapters.Console do
use Alice.Adapter
alias Alice.Adapters.Console.Connection

@doc false
def init({bot, opts}) do
def connect(bot, opts) do
{:ok, conn} = Connection.start(opts)
send(self(), :connected)
{:ok, %{conn: conn, opts: opts, bot: bot}}
end

@doc false
def handle_cast({:reply, msg}, %{conn: conn} = state) do
send(conn, {:reply, msg})
{:noreply, state}
def handle_outgoing(%Alice.Message{text: text}, %{conn: conn} = state) do
send(conn, {:reply, text})
{:ok, state}
end

@doc false
def handle_info(:connected, %{bot: bot} = state) do
:ok = Alice.Bot.handle_connect(bot)
{:noreply, state}
end
def handle_info({:message, msg}, %{bot: bot} = state) do
Alice.Bot.handle_in(bot, make_msg(bot, msg))
{:noreply, state}
def handle_incoming(ext_msg, %{bot: bot} = state) do
msg = make_msg(bot, ext_msg)
{:ok, msg, state}
end

defp make_msg(bot, %{"text" => text, "user" => user}) do
Expand Down
36 changes: 17 additions & 19 deletions lib/alice/adapters/noop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ defmodule Alice.Adapters.NoOp do
@moduledoc """
Alice No Op Adapter

The console adapter doesn't do anything.
The NoOp adapter doesn't do anything other than logging. It may be useful
for testing and debugging. It is also a decent example of the simplest
possible Alice adapter.

config :my_app, MyApp.Bot,
adapter: Alice.Adapters.NoOp,
Expand All @@ -13,35 +15,31 @@ defmodule Alice.Adapters.NoOp do
"""
use Alice.Adapter

@doc false
def init({bot, _opts}) do
send(self(), :connected)
def connect(bot, _opts) do
{:ok, bot}
end

@doc false
def handle_cast({:reply, msg}, bot) do
IO.puts "got reply message #{inspect msg} from bot #{inspect bot}"
{:noreply, bot}
def handle_connect(bot) do

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be handle_connected

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed my mind... this should be handle_connect
It makes more sense in the context of everything else.

IO.puts "NoOp connected"
{:ok, bot}
end

@doc false
def handle_info(:connected, bot) do
:ok = Alice.Bot.handle_connect(bot)
IO.puts "NoOp connected"
{:noreply, bot}
def handle_outgoing(msg, bot) do
IO.puts "got outgoing message #{inspect msg} from bot #{inspect bot}"
{:ok, bot}
end
def handle_info({:message, msg}, bot) do
IO.puts "received message #{inspect msg} for bot #{inspect bot}"
Alice.Bot.handle_in(bot, make_msg(bot, msg))
{:noreply, bot}

def handle_incoming(text, bot) do
IO.puts "received message #{inspect text} for bot #{inspect bot}"
msg = make_msg(bot, text)
{:ok, msg, bot}
end

defp make_msg(bot, msg) do
defp make_msg(bot, text) do
%Alice.Message{
ref: make_ref(),
bot: bot,
text: msg,
text: text,
room: "noop",
adapter: {__MODULE__, self()},
type: "chat",
Expand Down
22 changes: 8 additions & 14 deletions lib/alice/adapters/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,22 @@ defmodule Alice.Adapters.Test do

use Alice.Adapter

def init({bot, opts}) do
GenServer.cast(self(), :after_init)
def connect(bot, opts) do
{:ok, %{conn: nil, opts: opts, bot: bot}}
end

def handle_cast(:after_init, %{bot: bot} = state) do
Alice.Bot.handle_connect(bot)
{:noreply, state}
end

def handle_cast({:reply, msg}, %{conn: conn} = state) do
def handle_outgoing(%Alice.Message{} = msg, %{conn: conn} = state) do
send(conn, {:reply, msg})
{:noreply, state}
{:ok, state}
end

def handle_info({:message, msg}, %{bot: bot} = state) do
def handle_incoming(msg, %{bot: bot} = state) do
msg = %Alice.Message{bot: bot, adapter: {__MODULE__, self()}, text: msg.text, user: msg.user}
Alice.Bot.handle_in(bot, msg)
{:noreply, state}
{:ok, msg, state}
end
def handle_info(msg, %{bot: bot} = state) do

def handle_info({:message, msg}, {bot, state}) do
Alice.Bot.handle_in(bot, msg)
{:noreply, state}
{:noreply, {bot, state}}
end
end
35 changes: 18 additions & 17 deletions lib/alice/bot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,18 @@ defmodule Alice.Bot do

defp register_adapters(opts) do
{adapters, opts} = Keyword.pop(opts, :adapters, [])
GenServer.cast(self(), {:register_adapters, adapters, opts})
adapters = for adapter <- ensure_adapter(adapters) do
case adapter do
{name, mod} ->
{:ok, pid} = Supervisor.start_child(Alice.Adapter.Supervisor, [mod, self(), opts])
ProcessUtils.register_eventually(pid, name)
mod
mod when is_atom(mod) ->
{:ok, _pid} = Supervisor.start_child(Alice.Adapter.Supervisor, [mod, self(), opts])
mod
end
end
GenServer.cast(self(), {:update_adapters, adapters})
{adapters, opts}
end

Expand All @@ -179,7 +190,8 @@ defmodule Alice.Bot do
Alice.Handler.Supervisor
|> Supervisor.which_children()
|> Enum.map(fn({_,pid,_,_}) ->
{_,[{_,{mod,_,_}}|_]} = Process.info(pid, :dictionary)
{:dictionary, info} = Process.info(pid, :dictionary)
{mod, _, _} = Keyword.get(info, :"$initial_call")
{mod, pid}
end)
{:reply, handler_processes, state}
Expand All @@ -202,8 +214,8 @@ defmodule Alice.Bot do
end

def handle_cast({:reply, msg}, state) do
{adapter_mod, adapter_pid} = msg.adapter
adapter_mod.reply(adapter_pid, msg)
{_mod, pid} = msg.adapter
Alice.Adapter.reply(pid, msg)
{:noreply, state}
end
def handle_cast({:handle_in, msg}, state) do
Expand Down Expand Up @@ -232,22 +244,11 @@ defmodule Alice.Bot do
handlers = ensure_builtin_handlers(handlers)
for handler <- handlers do
{:ok, pid} = Supervisor.start_child(Alice.Handler.Supervisor, [handler, {name, self()}])
ProcessUtils.register_eventually(pid, handler)
# ProcessUtils.register_eventually(pid, handler)
end
{:noreply, %{state | handlers: handlers}}
end
def handle_cast({:register_adapters, adapters, opts}, state) do
adapters = for adapter <- ensure_adapter(adapters) do
case adapter do
{name, mod} ->
{:ok, pid} = Supervisor.start_child(Alice.Adapter.Supervisor, [mod, self(), opts])
ProcessUtils.register_eventually(pid, name)
mod
mod when is_atom(mod) ->
{:ok, _pid} = Supervisor.start_child(Alice.Adapter.Supervisor, [mod, self(), opts])
mod
end
end
def handle_cast({:update_adapters, adapters}, state) do
{:noreply, %{state | adapters: adapters}}
end

Expand Down
Loading