Fred T-H avatar Fred T-H committed 9dbb59e

renaming files to make it easier to fit into an otp app

Comments (0)

Files changed (22)

src/chut_SUITE.erl

+%%%-------------------------------------------------------------------
+%%% File    : chut_user_SUITE.erl
+%%% Description: Basic tests for a user and its clients
+%%%
+%%% Created : 2009/12/23
+%%%-------------------------------------------------------------------
+-module(chut_SUITE).
+-compile(export_all).
+-include_lib("common_test/include/ct.hrl").
+%%--------------------------------------------------------------------
+%% Function: suite() -> Info
+%% Info = [tuple()]
+%%--------------------------------------------------------------------
+suite() ->
+    [{timetrap,{seconds,30}}].
+%%--------------------------------------------------------------------
+%% Function: init_per_suite(Config0) ->
+%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+init_per_suite(Config) ->
+    Config.
+%%--------------------------------------------------------------------
+%% Function: end_per_suite(Config0) -> void() | {save_config,Config1}
+%% Config0 = Config1 = [tuple()]
+%%--------------------------------------------------------------------
+end_per_suite(_Config) ->
+    ok.
+%%--------------------------------------------------------------------
+%% Function: init_per_group(GroupName, Config0) ->
+%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
+%% GroupName = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+init_per_group(_GroupName, Config) ->
+    Config.
+%%--------------------------------------------------------------------
+%% Function: end_per_group(GroupName, Config0) ->
+%%               void() | {save_config,Config1}
+%% GroupName = atom()
+%% Config0 = Config1 = [tuple()]
+%%--------------------------------------------------------------------
+end_per_group(_GroupName, _Config) ->
+    ok.
+%%--------------------------------------------------------------------
+%% Function: init_per_testcase(TestCase, Config0) ->
+%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
+%% TestCase = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+init_per_testcase(_TestCase, Config) ->
+    Config.
+%%--------------------------------------------------------------------
+%% Function: end_per_testcase(TestCase, Config0) ->
+%%               void() | {save_config,Config1} | {fail,Reason}
+%% TestCase = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+end_per_testcase(_TestCase, _Config) ->
+    ok.
+%%--------------------------------------------------------------------
+%% Function: groups() -> [Group]
+%% Group = {GroupName,Properties,GroupsAndTestCases}
+%% GroupName = atom()
+%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}]
+%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase]
+%% TestCase = atom()
+%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}}
+%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail |
+%%              repeat_until_any_ok | repeat_until_any_fail
+%% N = integer() | forever
+%%--------------------------------------------------------------------
+groups() ->
+    [].
+%%--------------------------------------------------------------------
+%% Function: all() -> GroupsAndTestCases | {skip,Reason}
+%% GroupsAndTestCases = [{group,GroupName} | TestCase]
+%% GroupName = atom()
+%% TestCase = atom()
+%% Reason = term()
+%%--------------------------------------------------------------------
+all() -> 
+    [chut_user_timeout, chut_user_subscribe, chut_user_message, chut_user_relay, chut_user_conv,
+     chut_user_hist,
+     chut_client_conv, two_chut_clients_one_chut_user, chut_client_aware_crashed_user,
+     chut_client_hist].
+
+%%--------------------------------------------------------------------
+%% Function: chut_user_timeout(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: tests that a user without any clients attached to it
+%% times out in due time.
+%%--------------------------------------------------------------------
+chut_user_timeout(_Config) ->
+    [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
+    IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
+    chut_user:start(Id, Sleep, 0),
+    true = lists:all(IsProc, Expected),
+    timer:sleep(Sleep*3),
+    io:format("procs: ~p~n", [global:registered_names()]),
+    false = lists:any(IsProc, Expected),
+    ok.
+
+%%--------------------------------------------------------------------
+%% Function: chut_user_subscribe(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Checks that a client listening to a user keeps it from
+%% timing out, and that once it stops listening, the user does time out.
+%%--------------------------------------------------------------------
+chut_user_subscribe(_Config) ->
+    [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
+    IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
+    chut_user:start(Id, Sleep, 0),
+    {ok, Handler} = chut_user:subscribe(Id),
+    timer:sleep(Sleep*3),
+    true = lists:all(IsProc, Expected),
+    chut_user:unsubscribe(Id, Handler),
+    process_flag(trap_exit, true),
+    timer:sleep(Sleep*3),
+    false = lists:any(IsProc, Expected),
+    receive
+        {'EXIT',_Pid, killed} -> ok
+    after 500 ->
+        io:format("Process won't die after the event manager did!~n")
+    end,
+    ok.
+
+%%--------------------------------------------------------------------
+%% Function: chut_user_message(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Tests that a client's listener notifies said client
+%% whenever a message is sent from the user the listener is attached
+%% to.
+%%--------------------------------------------------------------------
+chut_user_message(_Config) ->
+    chut_user:start(send_id, 500, 0),
+    {ok, Handler} = chut_user:subscribe(send_id),
+    chut_user:message(send_id, invalid_id, hello),
+    X = receive
+            {{send_id, Handler}, {sent, invalid_id, hello}} -> true
+        after 1000 -> throw("Message not sent")
+        end,
+    chut_user:unsubscribe(send_id, Handler),
+    true = X.
+
+%%--------------------------------------------------------------------
+%% Function: chut_user_relay(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Confirms that a client listening to a user that
+%% receives a message also receives the same message.
+%%--------------------------------------------------------------------
+chut_user_relay(_Config) ->
+    chut_user:start(sender, 500, 0),
+    {ok, HandlerS} = chut_user:subscribe(sender),
+    chut_user:start(rec, 500, 0),
+    {ok, HandlerR} = chut_user:subscribe(rec),
+    chut_user:message(sender, rec, hello),
+    X = receive
+            {{rec,HandlerR}, {received, sender, hello}} -> true
+        after 1000 -> throw("Message not received")
+        end,
+    chut_user:unsubscribe(sender, HandlerS),
+    chut_user:unsubscribe(rec, HandlerR),
+    true = X.
+
+%%--------------------------------------------------------------------
+%% Function: chut_user_conv(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Simulates a conversation between two different users
+%% with one client each.
+%%--------------------------------------------------------------------
+chut_user_conv(_Config) ->
+    {A,B} = {make_ref(), make_ref()},
+    chut_user:start(A, 500, 0),
+    {ok, HandlerA} = chut_user:subscribe(A),
+    chut_user:start(B, 500, 0),
+    {ok, HandlerB} = chut_user:subscribe(B),
+    chut_user:message(A,B,"Hello, B"),
+    chut_user:message(B,A,"Oh, Hello, A!"),
+    receive {{B,HandlerB}, {received, A, "Hello, B"}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {{A,HandlerA}, {sent, B, "Hello, B"}} -> ok
+    after 500 -> throw("Message not sent")
+    end,
+    receive {{A,HandlerA}, {received, B, "Oh, Hello, A!"}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {{B,HandlerB}, {sent, A, "Oh, Hello, A!"}} -> ok
+    after 500 -> throw ("Message not sent")
+    end,
+    chut_user:message(A,B, "Well, goodbye!"),
+    chut_user:message(B,A, "Have a nice day..."),
+    receive {{B,HandlerB}, {received, A, "Well, goodbye!"}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {{A,HandlerA}, {sent, B, "Well, goodbye!"}} -> ok
+    after 500 -> throw("Message not sent")
+    end,
+    receive {{A,HandlerA}, {received, B, "Have a nice day..."}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {{B,HandlerB}, {sent, A, "Have a nice day..."}} -> ok
+    after 500 -> throw ("Message not sent")
+    end,
+    chut_user:unsubscribe(A, HandlerA),
+    chut_user:unsubscribe(B, HandlerB).
+
+
+%%--------------------------------------------------------------------
+%% Function: chut_user_hist(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Simulates a short conversation and checks that a
+%% history log can be obtained.
+%%--------------------------------------------------------------------
+chut_user_hist(_Config) ->
+    {A,B} = {make_ref(), make_ref()},
+    chut_user:start(A, 500, 2),
+    {ok, HandlerA} = chut_user:subscribe(A),
+    chut_user:start(B, 500, 0),
+    {ok, HandlerB} = chut_user:subscribe(B),
+    [] = chut_user:history(A),
+    chut_user:message(A,B,"Hello, B"),
+    chut_user:message(B,A,"Oh, Hello, A!"),
+    timer:sleep(500),
+    [{sent,B,"Hello, B"},
+     {received,B,"Oh, Hello, A!"}] = chut_user:history(A),
+    chut_user:message(A,B,"Hello2, B"),
+    timer:sleep(500),
+    [{received,B,"Oh, Hello, A!"},
+     {sent,B,"Hello2, B"}] = chut_user:history(A),
+    chut_user:unsubscribe(A,HandlerA),
+    chut_user:unsubscribe(B,HandlerB).
+
+%%--------------------------------------------------------------------
+%% Function: chut_client_conv(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Simulates a conversation between two different users
+%% with one client each, through the chut_client module this time.
+%%--------------------------------------------------------------------
+chut_client_conv(_Config) ->
+    {A,B} = {make_ref(), make_ref()},
+    {ok, ConnA} = chut_client:connect(A),
+    {ok, ConnB} = chut_client:connect(B),
+    chut_client:message(A,B,"hi"),
+    [{sent, B, "hi"}] = chut_client:listen(A,ConnA),
+    chut_client:message(A,B, "you there?"),
+    timer:sleep(50), %% simulate delays to group messages
+    [{received, A, "hi"},{received, A, "you there?"}] = chut_client:listen(B,ConnB),
+    chut_client:message(B,A, "stop harassing me!"),
+    timer:sleep(50), %% simulate delays to group messages
+    [{sent, B, "you there?"},
+     {received,B, "stop harassing me!"}] = chut_client:listen(A,ConnA),
+    [{sent, A, "stop harassing me!"}] = chut_client:listen(B,ConnB),
+    chut_client:disconnect(A,ConnA),
+    chut_client:disconnect(B,ConnB).
+
+%%--------------------------------------------------------------------
+%% Function: two_chut_clients_one_chut_user(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: tests that two clients can be listening to the same
+%% user within the same process.
+%%--------------------------------------------------------------------
+two_chut_clients_one_chut_user(_Config) ->
+    A = make_ref(),
+    {ok, ConnA} = chut_client:connect(A),
+    {ok, ConnB} = chut_client:connect(A),
+    chut_client:message(A,A,"hi"),
+    [{sent, A, "hi"},{received, A, "hi"}] = chut_client:listen(A,ConnA),
+    [{sent, A, "hi"},{received, A, "hi"}] = chut_client:listen(A,ConnB),
+    chut_client:disconnect(A,ConnA),
+    chut_client:disconnect(A,ConnB).
+
+%%--------------------------------------------------------------------
+%% Function: chut_client_aware_crashed_user(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Checks that a client listening to a given user is
+%% notified of the user's death and exits the same way.
+%%--------------------------------------------------------------------
+chut_client_aware_crashed_user(_Config) ->
+    A = make_ref(),
+    {ok, H} = chut_client:connect(A),
+    chut_user:terminate(A,kill),
+    try chut_client:listen(A,H) of
+        _ -> throw(no_dead_signal)
+    catch
+        exit:Reason -> Reason
+    end.
+
+%%--------------------------------------------------------------------
+%% Function: chut_client_hist(_) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Reason = term()
+%% Comment = term()
+%% Description: Checks that a client can receive the history log from
+%% the user processes. Should just be a wrapper around chut_user:history/1
+%%--------------------------------------------------------------------
+chut_client_hist(_Config) ->
+    {A,B} = {make_ref(),make_ref()},
+    {ok, HA} = chut_client:connect(A),
+    {ok, HB} = chut_client:connect(B),
+    [] = chut_client:history(B),
+    chut_client:message(A,B,"hah!"),
+    chut_client:message(A,B,"hoh!"),
+    timer:sleep(200),
+    [{sent,B,"hah!"},{sent,B,"hoh!"}] = chut_client:history(A),
+    chut_client:disconnect(A,HA),
+    chut_client:disconnect(B,HB).

src/chut_client.erl

+%%%===================================================================
+%%% The client module is an additional abstraction step over the chut_user
+%%% module, focused on tasks frequently used by a client process.
+%%%===================================================================
+-module(chut_client).
+-export([connect/1, disconnect/2, message/3, listen/2, history/1]).
+
+-define(SESSION_LIFE, 15000).
+-define(LISTEN_TIMEOUT, 25000). %% set to 25s; opera dies after 30s.
+-define(HISTORY_LIMIT, 10).
+
+%%--------------------------------------------------------------------
+%% Function: connect(Id) -> {ok, HandlerId}.
+%% Description: Let's a client initiate a connection to some user.
+%% If the user doesn't exist, it should be started, otherwise the
+%% client connects normally.
+%% The client should keep track of the HandlerId as it acts a bit as
+%% its session ID for a given user.
+%%--------------------------------------------------------------------
+connect(Id) ->
+    chut_user:start(Id, ?SESSION_LIFE, ?HISTORY_LIMIT),
+    chut_user:subscribe(Id).
+
+%%--------------------------------------------------------------------
+%% Function: disconnect(Id, HandlerId) -> ok
+%% Description: Removes a client's listener from a given user.
+%%--------------------------------------------------------------------
+disconnect(Id, HandlerId) ->
+    chut_user:unsubscribe(Id, HandlerId).
+
+%%--------------------------------------------------------------------
+%% Function: message(From, To, Message) -> ok
+%% Description: Sends a message from a given user to another one
+%%--------------------------------------------------------------------
+message(From, To, Message) ->
+    chut_user:message(From, To, Message).
+
+%%--------------------------------------------------------------------
+%% Function: listen(Id, HandlerId) -> [Message]
+%% Description: Fetches messages belonging to user Id from HandlerId.
+%% If the user has been terminated for some reason, there will be
+%% an exit exception raised.
+%%--------------------------------------------------------------------
+listen(Id, HandlerId) ->
+    receive
+        {{Id, HandlerId}, Msg} -> [Msg | listen1(Id, HandlerId)];
+        {gen_event_EXIT, HandlerId, Reason} ->
+            case Reason of
+                normal -> ok;
+                Reason -> exit(Reason)
+            end
+    after ?LISTEN_TIMEOUT ->
+        []
+    end.
+
+listen1(Id, HandlerId) ->
+    receive
+        {{Id, HandlerId}, Msg} -> [Msg | listen1(Id, HandlerId)]
+    after 0 ->
+        []
+    end.
+
+%%--------------------------------------------------------------------
+%% Function: history(Id) -> [Message]
+%% Description: Fetches previous messages that were routed through
+%% the user.
+%%--------------------------------------------------------------------
+history(Id) -> chut_user:history(Id).

src/chut_user.erl

+%%%===================================================================
+%%% Module used to create an abstraction layer against all components
+%%% of a user.
+%%%===================================================================
+-module(chut_user).
+
+%% API
+-export([start/3, terminate/2, subscribe/1, unsubscribe/2, subscribers/1,
+         message/3, relay/3, history/1]).
+
+%%--------------------------------------------------------------------
+%% Function: Start(UserId, TimeOut, HistoryLimit) -> ok
+%% Description: registers a new user process or does nothing if it
+%% already is registered.
+%%--------------------------------------------------------------------
+start(UserId, TimeOut, HistoryLimit) ->
+    case global:whereis_name(UserId) of
+        undefined ->
+            %% start it here
+            chut_user_sup:start_link(UserId, TimeOut),
+            chut_user_manager:add_handler(UserId, chut_user_dispatch_handler, UserId),
+            chut_user_manager:add_handler(UserId, chut_user_history_handler, {UserId,HistoryLimit}),
+            io:format("started user process for id ~p~n",[UserId]),
+            started;
+        _Pid ->
+            io:format("connected to user ~p~n",[UserId]),
+            ok
+    end.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(UserId, Reason) -> true
+%% Description: kills the supervisor which should trickle down to the
+%% children.
+%%--------------------------------------------------------------------
+terminate(UserId, Reason) ->
+    Pid = global:whereis_name(UserId),
+    io:format("Killing user ~p. Reason: ~p~n",[UserId, Reason]),
+    erlang:exit(Pid, kill).
+
+%%--------------------------------------------------------------------
+%% Function: subscribe(UserId) -> {ok, HandlerId}
+%% Description: adds a listen handler to a gen_event and notifies the
+%% gen_fsm to track it.
+%%--------------------------------------------------------------------
+subscribe(UserId) ->
+    HandlerId = {chut_user_listen_handler, make_ref()},
+    chut_user_manager:add_sup_handler(UserId, HandlerId, {UserId, self(), HandlerId}),
+    chut_user_monitor:add_handler(UserId, HandlerId),
+    {ok, HandlerId}.
+
+%%--------------------------------------------------------------------
+%% Function: subscribers(UserId) -> [Subscriber]
+%% Description: returns a list of all the handlers attached to a given
+%% user.
+%%--------------------------------------------------------------------
+subscribers(UserId) ->
+    chut_user_manager:get_handlers(UserId).
+
+%%--------------------------------------------------------------------
+%% Function: unsubscribe(UserId, HandlerId) -> ok
+%% Description: removes a client's listen handler from a user.
+%%--------------------------------------------------------------------
+unsubscribe(UserId, HandlerId) ->
+    chut_user_manager:delete_handler(UserId, HandlerId, []),
+    chut_user_monitor:delete_handler(UserId, HandlerId),
+    ok.
+
+%%--------------------------------------------------------------------
+%% Function: message(From, To, Message) -> ok
+%% Description: Takes a message and routes it through the sending user
+%%--------------------------------------------------------------------
+message(From, To, Message) ->
+    chut_user_manager:notify(From, {send, To, Message}),
+    ok.
+
+%%--------------------------------------------------------------------
+%% Function: relay(From, To, Message) -> ok
+%% Description: Usually called by the dispatch handler. takes a sent
+%% message and routes it to the receiving user.
+%%--------------------------------------------------------------------
+relay(From, To, Message) ->
+    chut_user_manager:notify(To, {received, From, Message}),
+    ok.
+
+%%--------------------------------------------------------------------
+%% Function: history(UserId) -> [Messages]
+%% Description: Called by the client. Contacts an event handler
+%% that returns the last messages sent and received by UserId.
+%%--------------------------------------------------------------------
+history(UserId) ->
+    chut_user_manager:notify(UserId, {self(), history}),
+    receive
+        {UserId, {history, History}} ->
+            History
+        after 5000 ->
+            throw(no_history_response)
+    end.

src/chut_user_dispatch_handler.erl

+%%%===================================================================
+%%% This event handler is to be attached only once per user process.
+%%% Its role is to route message from a user to another one. Without
+%%% this handler, no outbound messages can be sent.
+%%%===================================================================
+-module(chut_user_dispatch_handler).
+-compile(export_all).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(UserId) -> {ok, UserId}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler.
+%%--------------------------------------------------------------------
+init(UserId) ->
+    {ok, UserId}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%%                               {swap_handler, Args1, State1, Mod2, Args2} |
+%%                               remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%
+%% The only notification supported by this handler is {send, To, Msg},
+%% which triggers a message transmission to another user.
+%%--------------------------------------------------------------------
+handle_event({send, To, Msg}, UserId) ->
+    chut_user:relay(UserId, To, Msg),
+    {ok, UserId};
+handle_event(_Event, State) ->
+    {ok, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%%                                {swap_handler, Reply, Args1, State1,
+%%                                  Mod2, Args2} |
+%%                                {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Request, State) ->
+    Reply = ok,
+    {ok, Reply, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%%                             {swap_handler, Args1, State1, Mod2, Args2} |
+%%                              remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+  {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description:Whenever an event handler is deleted from an event manager,
+%% this function is called. It should be the opposite of Module:init/1 and
+%% do any necessary cleaning up.
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+  ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.
+

src/chut_user_history_handler.erl

+%%%===================================================================
+%%% This event handler is to be attached once per user.
+%%% Its role is to log messages routed through a user and
+%%% message them back to a client on demand.
+%%%===================================================================
+-module(chut_user_history_handler).
+-compile(export_all).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init({UserId, Limit}) -> {ok, {UserId, Limit, []}}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler. The state
+%% is constituted of the User's Id (to couple it to messages) and
+%% a limit (how many messages need to be kept in history).
+%%--------------------------------------------------------------------
+init({UserId, Limit}) ->
+    {ok, {UserId, Limit, []}}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%%                               {swap_handler, Args1, State1, Mod2, Args2} |
+%%                               remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%
+%% Handles two kind of messages: received and sent messages. It wraps
+%% them with a tuple containing the handler's id and the user's id
+%% so many clients can be within a single process.
+%%--------------------------------------------------------------------
+handle_event(Event={received, _, _}, {UserId, Limit, History}) ->
+    {ok, {UserId, Limit, add_to_history(Event, History, Limit)}};
+handle_event({send,To,Msg}, {UserId, Limit, History}) ->
+    Event = {sent, To, Msg},
+    {ok, {UserId, Limit, add_to_history(Event, History, Limit)}};
+handle_event({From, history}, S={UserId, _, History}) ->
+    From ! {UserId, {history, lists:reverse(History)}},
+    {ok, S};
+handle_event(_Event, State) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%%                                {swap_handler, Reply, Args1, State1,
+%%                                  Mod2, Args2} |
+%%                                {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Request, State) ->
+    Reply = ok,
+    {ok, Reply, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%%                             {swap_handler, Args1, State1, Mod2, Args2} |
+%%                              remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+  {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description:Whenever an event handler is deleted from an event manager,
+%% this function is called. It should be the opposite of Module:init/1 and
+%% do any necessary cleaning up.
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+  ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.
+
+%%===================================================================
+%% Internal functions
+%%===================================================================
+%%-------------------------------------------------------------------
+%% Function: add_to_history(Event, History, Limit) -> NewHistory
+%% Description: prepends a new event to the history (a list of
+%% events). If the list gets larger than the given limit, it is
+%% truncated.
+%%-------------------------------------------------------------------
+add_to_history(_, _, 0) -> [];
+add_to_history(Event, History, Limit) ->
+    case length(History) of
+        X when X >= Limit-1 ->
+            [Event|lists:sublist(History,Limit-1)];
+        _ ->
+            [Event|History]
+    end.

src/chut_user_listen_handler.erl

+%%%===================================================================
+%%% This event handler is to be attached by every client of a user.
+%%% Its role is to redirect messages routed through a user to its
+%%% own client.
+%%%===================================================================
+-module(chut_user_listen_handler).
+-compile(export_all).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(State={UserId, SubsriberPid, HandlerId}) -> {ok, State}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler. The state
+%% is constituted of the User's Id (to couple it to messages), 
+%% the client's pid (to send it messages) and a unique HandlerId (to
+%% handle a client's termination and identify messages).
+%%--------------------------------------------------------------------
+init({UserId, SubscriberPid, HandlerId}) when is_pid(SubscriberPid) ->
+    {ok, {UserId, SubscriberPid, HandlerId}}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%%                               {swap_handler, Args1, State1, Mod2, Args2} |
+%%                               remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%
+%% Handles two kind of messages: received and sent messages. It wraps
+%% them with a tuple containing the handler's id and the user's id
+%% so many clients can be within a single process.
+%%--------------------------------------------------------------------
+handle_event({received, From, Msg}, {UserId, SubscriberPid, HandlerId}) ->
+    SubscriberPid ! {{UserId, HandlerId}, {received, From, Msg}},
+    {ok, {UserId, SubscriberPid, HandlerId}};
+handle_event({send, To, Msg}, {UserId, SubscriberPid, HandlerId}) ->
+    SubscriberPid ! {{UserId, HandlerId}, {sent, To, Msg}},
+    {ok, {UserId, SubscriberPid, HandlerId}};
+handle_event(_Event, State) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%%                                {swap_handler, Reply, Args1, State1,
+%%                                  Mod2, Args2} |
+%%                                {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Request, State) ->
+    Reply = ok,
+    {ok, Reply, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%%                             {swap_handler, Args1, State1, Mod2, Args2} |
+%%                              remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+  {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description:Whenever an event handler is deleted from an event manager,
+%% this function is called. It should be the opposite of Module:init/1 and
+%% do any necessary cleaning up.
+%%
+%% Whenever the termination reason is stop (the client is dead), the
+%% handler is removed. This is a call that's a bit ugly, because it
+%% goes straight to the fsm in order to make sure no dead handlers
+%% are counted.
+%% It also removes the link between the client and the manager to avoid
+%% killing process pools for no reason.
+%%--------------------------------------------------------------------
+terminate({stop,_}, {UserId, SubscriberPid, HandlerId}) ->
+  chut_user_monitor:delete_handler(UserId, HandlerId),
+  unlink(SubscriberPid),
+  ok;
+terminate(_Reason, _State) ->
+  ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.

src/chut_user_manager.erl

+%%%===================================================================
+%%% This event manager is part of the 3 processes making a user up.
+%%% It is responsible of holding all the handlers having to do with
+%%% message handling.
+%%%===================================================================
+-module(chut_user_manager).
+
+%% API
+-export([start_link/1, add_handler/3, add_sup_handler/3, delete_handler/3,
+         notify/2, get_handlers/1]).
+
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | {error,Error}
+%% Description: Creates an event manager.
+%% The user is global by default as we ought to support a multiple
+%% node system and will need more than a single process
+%%--------------------------------------------------------------------
+start_link(UserId) ->
+    gen_event:start_link({global, {manager, UserId}}).
+
+%%--------------------------------------------------------------------
+%% Function: add_handler(Module,Args) -> ok | {'EXIT',Reason} | term()
+%% Description: Adds an event handler
+%%--------------------------------------------------------------------
+add_handler(UserId, Handler = {_Module, _HandlerId}, Params) ->
+    gen_event:add_handler({global, {manager, UserId}}, Handler, Params);
+add_handler(UserId, Module, Params) when is_atom(Module) ->
+    gen_event:add_handler({global, {manager, UserId}}, Module, Params).
+
+%%--------------------------------------------------------------------
+%% Function: add_sup_handler(Module,Args) -> ok | {'EXIT',Reason} | term()
+%% Description: Adds an event handler that watches for the calling process'
+%% exit signals. This avoids having zombie listeners left over. The actual
+%% handling of errors is done in the callback module's terminate/2 function
+%%--------------------------------------------------------------------
+add_sup_handler(UserId, Handler = {_Module, _HandlerId}, Params) ->
+    gen_event:add_sup_handler({global, {manager, UserId}}, Handler, Params);
+add_sup_handler(UserId, Module, Params) when is_atom(Module) ->
+    gen_event:add_sup_handler({global, {manager, UserId}}, Module, Params).
+
+%%--------------------------------------------------------------------
+%% Function: delete_handler(UserId, Handler, Params) ->
+%% Description: Removes an event handler
+%%--------------------------------------------------------------------
+delete_handler(UserId, Handler, Params) ->
+    gen_event:delete_handler({global, {manager,UserId}}, Handler, Params).
+
+%%--------------------------------------------------------------------
+%% Function: get_handlers(UserId) -> [term()]
+%% Description: returns a list of all the handlers registered
+%%--------------------------------------------------------------------
+get_handlers(UserId) ->
+    gen_event:which_handlers({global, {manager, UserId}}).
+
+%%--------------------------------------------------------------------
+%% Function: notify(Event) -> ok | {error, Reason}
+%% Description: Sends the Event through the event manager.
+%%--------------------------------------------------------------------
+notify(UserId, Message) ->
+  gen_event:notify({global, {manager,UserId}}, Message).

src/chut_user_monitor.erl

+%%%===================================================================
+%%% chut_user_monitor is an fsm responsible for keeping the user alive when
+%%% no clients are connected. It's also responsible for a user timing
+%%% out when clients haven't connected for a given wait time.
+%%% Eventually, this might be used to monitor other users the current
+%%% user is having a conversation with so when they log off, the current
+%%% user knows.
+%%%===================================================================
+-module(chut_user_monitor).
+-behaviour(gen_fsm).
+
+%% API
+-export([start_link/3, add_handler/2, delete_handler/2]).
+
+%% gen_fsm callbacks
+-export([init/1, waiting/2, listening/2, handle_event/3,
+         handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link(UserId, Dispatcher, TimeOut) ->
+%%               {ok,Pid} | ignore | {error,Error}
+%% Description:Creates a gen_fsm process which calls Module:init/1 to
+%% initialize. To ensure a synchronized start-up procedure, this function
+%% does not return until Module:init/1 has returned.
+%%
+%% In this one, TimeOut is the time to live of the user when there are
+%% no listen_handlers connected to the event manager.
+%% The Dispatcher is yet unused, but will be when monitoring of users
+%% is added in (no YAGNI rethinking needed, I hope!)
+%% The UserId is needed for the fsm to terminate its supervisor when
+%% the time is out.
+%%--------------------------------------------------------------------
+start_link(UserId, Dispatcher, TimeOut) ->
+  gen_fsm:start_link({global, {monitor,UserId}},
+                     ?MODULE,
+                     [{id, UserId},
+                      {dispatcher, Dispatcher},
+                      {timeout, TimeOut}],
+                     []).
+
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: add_handler(UserId, HandlerId) -> ok
+%% Description: Sends a message to the FSM to start tracking a new
+%% client listener.
+%%--------------------------------------------------------------------
+add_handler(UserId, HandlerId) ->
+    gen_fsm:sync_send_all_state_event({global, {monitor,UserId}}, {add_handler, HandlerId}).
+
+%%--------------------------------------------------------------------
+%% Function: delete_handler(UserId, HandlerId) -> ok
+%% Description: Orders the fsm to stop tracking the client listener.
+%% If there are no listeners left, the FSM will switch into waiting
+%% mode and timeout if there are no new handlers.
+%%--------------------------------------------------------------------
+delete_handler(UserId, HandlerId) ->
+    gen_fsm:send_event({global, {monitor,UserId}}, {delete_handler, HandlerId}).
+
+%%====================================================================
+%% gen_fsm callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, StateName, State} |
+%%                         {ok, StateName, State, Timeout} |
+%%                         ignore                              |
+%%                         {stop, StopReason}
+%% Description:Whenever a gen_fsm is started using gen_fsm:start/[3,4] or
+%% gen_fsm:start_link/3,4, this function is called by the new process to
+%% initialize.
+%% Added to the state declared in start_link is an empty list of
+%% handlers.
+%%--------------------------------------------------------------------
+init(Opts) ->
+    {timeout, T} = proplists:lookup(timeout,Opts),
+    {ok, waiting, [{handlers, []} | Opts], T}.
+
+
+%%--------------------------------------------------------------------
+%% Function: waiting(timeout, State) -> {stop, normal, State}
+%% Description: the fsm waits until it times out, in which case it
+%% will kill its supervisor. Adding a handler (global event) will stop
+%% the waiting state
+%%--------------------------------------------------------------------
+waiting(timeout, State) ->
+    %% maybe store messages?
+    io:format("timing out~n"),
+    {id, UserId} = proplists:lookup(id, State),
+    chut_user:terminate(UserId, timeout), % take care, circular dependency!
+    {stop, normal, State};
+waiting(_Event, State) -> % catch-all to avoid crashing
+    io:format("invalid timeout msg:~p~n",[_Event]),
+    {timeout, TimeOut} = proplists:lookup(timeout, State),
+    {next_state, waiting, State, TimeOut}.
+
+%%--------------------------------------------------------------------
+%% function: listening({delete_handler, HandlerId}, State) ->
+%%              {next_state, waiting, NewState, TimeOut} |
+%%              {next_state, listening, NewState}
+%% Description: the fsm just keeps listening for the order to stop
+%% tracking event handlers. When no handler is left, switch to the
+%% waiting state, otherwise, keep listening.
+%%--------------------------------------------------------------------
+listening({delete_handler, HandlerId}, State) ->
+    {handlers, Handlers} = proplists:lookup(handlers, State),
+    {timeout, TimeOut} = proplists:lookup(timeout, State),
+    TempState = proplists:delete(handlers, State),
+    NewHandlers = lists:delete(HandlerId, Handlers),
+    NewState = [{handlers, NewHandlers} | TempState],
+    case NewHandlers of
+        [] ->    {next_state, waiting, NewState, TimeOut};
+        [_|_] -> {next_state, listening, NewState}
+    end;
+listening(_Event, State) -> % avoid crashes!
+    {next_state, listening, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, StateName, State) -> {next_state, NextStateName,
+%%                                                NextState} |
+%%                                          {next_state, NextStateName,
+%%                                                NextState, Timeout} |
+%%                                          {stop, Reason, NewState}
+%% Description: Whenever a gen_fsm receives an event sent using
+%% gen_fsm:send_all_state_event/2, this function is called to handle
+%% the event.
+%%--------------------------------------------------------------------
+handle_event(_Event, StateName, State) ->
+  {next_state, StateName, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_sync_event(Event, From, StateName,
+%%                   State) -> {next_state, NextStateName, NextState} |
+%%                             {next_state, NextStateName, NextState,
+%%                              Timeout} |
+%%                             {reply, Reply, NextStateName, NextState}|
+%%                             {reply, Reply, NextStateName, NextState,
+%%                              Timeout} |
+%%                             {stop, Reason, NewState} |
+%%                             {stop, Reason, Reply, NewState}
+%% Description: Whenever a gen_fsm receives an event sent using
+%% gen_fsm:sync_send_all_state_event/2,3, this function is called to handle
+%% the event.
+%%
+%% When receiving {add_handler, HandlerId} as an event, this will
+%% make the fsm start tracking it and switch the state to listening.
+%%--------------------------------------------------------------------
+handle_sync_event({add_handler, HandlerId}, _From, _StateName, State) ->
+    {handlers, Handlers} = proplists:lookup(handlers, State),
+    TempState = proplists:delete(handlers, State),
+    NewState = [{handlers, [HandlerId | Handlers]} | TempState],
+    {reply, ok, listening, NewState};
+handle_sync_event(_Event, _From, StateName, State) ->
+    Reply = ok,
+    {reply, Reply, StateName, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info,StateName,State)-> {next_state, NextStateName, NextState}|
+%%                                     {next_state, NextStateName, NextState,
+%%                                       Timeout} |
+%%                                     {stop, Reason, NewState}
+%% Description: This function is called by a gen_fsm when it receives any
+%% other message than a synchronous or asynchronous event
+%% (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, StateName, State) ->
+    {next_state, StateName, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, StateName, State) -> void()
+%% Description:This function is called by a gen_fsm when it is about
+%% to terminate. It should be the opposite of Module:init/1 and do any
+%% necessary cleaning up. When it returns, the gen_fsm terminates with
+%% Reason. The return value is ignored.
+%%--------------------------------------------------------------------
+terminate(_Reason, _StateName, _State) ->
+    ok.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% code_change(OldVsn, StateName, State, Extra) -> {ok, StateName, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, StateName, State, _Extra) ->
+    {ok, StateName, State}.

src/chut_user_sup.erl

+%%%===================================================================
+%%% Supervises a given user and acts as an entry point for the internal
+%%% FSM to kill the user.
+%%%===================================================================
+-module(chut_user_sup).
+-behaviour(supervisor).
+
+%% supervisor callback
+-export([init/1]).
+%% API
+-export([start_link/2]).
+
+start_link(UserId, TimeOut) ->
+    {ok,Pid} = supervisor:start_link({global, UserId}, ?MODULE, [UserId, TimeOut]),
+    unlink(Pid).
+
+
+%%--------------------------------------------------------------------
+%% Function: init([UserId, Timeout]) -> [ChildSpec]
+%% Description: Creates the children specification for the event
+%% manager and the fsm constituting a user. A one for all restart
+%% strategy is used because only if the FSM and the event handler can
+%% a user work properly.
+%%--------------------------------------------------------------------
+init([UserId, TimeOut]) ->
+    MonitorId = {monitor, UserId},
+    ManagerId = {manager, UserId},
+    MonitorSpec = {MonitorId,
+                   {chut_user_monitor, start_link, [UserId, ManagerId, TimeOut]},
+                   transient,
+                   TimeOut,
+                   worker,
+                   [chut_user_monitor]},
+    ManagerSpec = {ManagerId,
+                   {chut_user_manager, start_link, [UserId]},
+                   transient,
+                   TimeOut,
+                   worker,
+                   dynamic},
+    {ok, {{one_for_all, 1, TimeOut},
+         [MonitorSpec, ManagerSpec]}}.
+

src/chut_user_supersup.erl

+%% this is a supervisor of all the users' supervisors
+-module(chut_user_supersup).

src/chut_webserver.erl

+-module(chut_webserver).
+-behaviour(gen_server).
+-define(SERVER, ?MODULE).
+
+%% API
+-export([start_link/1, dispatch_requests/1, stop/0]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+start_link(Port) ->
+    gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).
+
+init([Port]) ->
+    process_flag(trap_exit, true),
+    io:format("~p (~p) starting...~n", [?MODULE, self()]),
+    mochiweb_http:start([{port, Port},
+                         {loop, fun(Req) -> dispatch_requests(Req) end}]),
+    {ok, []}.
+
+stop() ->
+    gen_server:cast(?SERVER, stop).
+
+dispatch_requests(Req) ->
+    Path = Req:get(path),
+    handle(Path, Req).
+
+handle_call(_Request, _From, State) ->
+    Reply = ok,
+    {reply, Reply, State}.
+
+handle_cast(stop, State) ->
+    {stop, normal, State};
+
+handle_cast(_Msg, State) ->
+    {noreply, State}.
+
+handle_info({'DOWN', _, _, {mochiweb_http, _}, _}, State) ->
+    {stop, normal, State};
+
+handle_info(_Info, State) ->
+    {noreply, State}.
+
+terminate(_Reason, _State) ->
+    mochiweb_http:stop(),
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%% Internal functions
+handle("/message", Req) ->
+    Params = Req:parse_qs(),
+    From = proplists:get_value("from", Params),
+    To = proplists:get_value("to", Params),
+    Message = proplists:get_value("msg", Params),
+    {ok, H} = chut_client:connect(From),
+    chut_client:message(From, To, Message),
+    chut_client:disconnect(From, H),
+    %% we clear the event messages because mochiweb re-uses the same procs
+    %% over and over again in a pool. Otherwise, we leak memory and when the
+    %% listen call gets the right message process, a backlog gets printed to
+    %% the user.
+    lib:flush_receive(),
+    reply(Req, mochijson2:encode(ok), Params);
+
+handle("/listen", Req) ->
+    Params = Req:parse_qs(),
+    Nick = proplists:get_value("id", Params),
+    {ok,Handler} = chut_client:connect(Nick),
+    Msgs = chut_client:listen(Nick,Handler),
+    JSON = mochijson2:encode([json_prepare(M) || M <- Msgs]),
+    chut_client:disconnect(Nick, Handler),
+    reply(Req, JSON, Params);
+
+handle("/history", Req) ->
+    Params = Req:parse_qs(),
+    Nick = proplists:get_value("id",Params),
+    {ok, Handler} = chut_client:connect(Nick),
+    Msgs = chut_client:history(Nick),
+    JSON = mochijson2:encode([json_prepare(M) || M <- Msgs]),
+    chut_client:disconnect(Nick, Handler),
+    reply(Req, JSON, Params);
+
+handle(Path, Req) ->
+    Params = Req:parse_qs(),
+    reply(Req, mochijson2:encode([error,list_to_binary(Path)]), Params).
+
+%% have to convert lists to binaries to be strings in json
+%% also gotta change the internal format of things to make it fit
+json_prepare({Verb=[_|_],To,Msg}) -> json_prepare({unicode:characters_to_binary(Verb),To,Msg});
+json_prepare({Verb,To=[_|_],Msg}) -> json_prepare({Verb,unicode:characters_to_binary(To),Msg});
+json_prepare({Verb,To,Msg=[_|_]}) -> json_prepare({Verb,To,unicode:characters_to_binary(Msg)});
+json_prepare({Verb,To,Msg}) ->
+    {struct, [{action,Verb},{to,To},{message,Msg}]}.
+
+reply(Req, JSON, Params) ->
+    Callback = proplists:get_value("callback", Params),
+    Req:respond({200, [{"Content-Type", "text/javascript"}],
+                [Callback,$(,JSON,$),$;]}).

src/client.erl

-%%%===================================================================
-%%% The client module is an additional abstraction step over the usr
-%%% module, focused on tasks frequently used by a client process.
-%%%===================================================================
--module(client).
--export([connect/1, disconnect/2, message/3, listen/2, history/1]).
-
--define(SESSION_LIFE, 15000).
--define(LISTEN_TIMEOUT, 25000). %% set to 25s; opera dies after 30s.
--define(HISTORY_LIMIT, 10).
-
-%%--------------------------------------------------------------------
-%% Function: connect(Id) -> {ok, HandlerId}.
-%% Description: Let's a client initiate a connection to some user.
-%% If the user doesn't exist, it should be started, otherwise the
-%% client connects normally.
-%% The client should keep track of the HandlerId as it acts a bit as
-%% its session ID for a given user.
-%%--------------------------------------------------------------------
-connect(Id) ->
-    usr:start(Id, ?SESSION_LIFE, ?HISTORY_LIMIT),
-    usr:subscribe(Id).
-
-%%--------------------------------------------------------------------
-%% Function: disconnect(Id, HandlerId) -> ok
-%% Description: Removes a client's listener from a given user.
-%%--------------------------------------------------------------------
-disconnect(Id, HandlerId) ->
-    usr:unsubscribe(Id, HandlerId).
-
-%%--------------------------------------------------------------------
-%% Function: message(From, To, Message) -> ok
-%% Description: Sends a message from a given user to another one
-%%--------------------------------------------------------------------
-message(From, To, Message) ->
-    usr:message(From, To, Message).
-
-%%--------------------------------------------------------------------
-%% Function: listen(Id, HandlerId) -> [Message]
-%% Description: Fetches messages belonging to user Id from HandlerId.
-%% If the user has been terminated for some reason, there will be
-%% an exit exception raised.
-%%--------------------------------------------------------------------
-listen(Id, HandlerId) ->
-    receive
-        {{Id, HandlerId}, Msg} -> [Msg | listen1(Id, HandlerId)];
-        {gen_event_EXIT, HandlerId, Reason} ->
-            case Reason of
-                normal -> ok;
-                Reason -> exit(Reason)
-            end
-    after ?LISTEN_TIMEOUT ->
-        []
-    end.
-
-listen1(Id, HandlerId) ->
-    receive
-        {{Id, HandlerId}, Msg} -> [Msg | listen1(Id, HandlerId)]
-    after 0 ->
-        []
-    end.
-
-%%--------------------------------------------------------------------
-%% Function: history(Id) -> [Message]
-%% Description: Fetches previous messages that were routed through
-%% the user.
-%%--------------------------------------------------------------------
-history(Id) -> usr:history(Id).

src/ct.hrl

-%% used to replace common_test's broken includes
--include_lib("test_server/include/test_server.hrl").
--compile({parse_transform,ct_line}).
-

src/usr.erl

-%%%===================================================================
-%%% Module used to create an abstraction layer against all components
-%%% of a user.
-%%%===================================================================
--module(usr).
-
-%% API
--export([start/3, terminate/2, subscribe/1, unsubscribe/2, subscribers/1,
-         message/3, relay/3, history/1]).
-
-%%--------------------------------------------------------------------
-%% Function: Start(UserId, TimeOut, HistoryLimit) -> ok
-%% Description: registers a new user process or does nothing if it
-%% already is registered.
-%%--------------------------------------------------------------------
-start(UserId, TimeOut, HistoryLimit) ->
-    case global:whereis_name(UserId) of
-        undefined ->
-            %% start it here
-            usr_sup:start_link(UserId, TimeOut),
-            usr_manager:add_handler(UserId, usr_dispatch_handler, UserId),
-            usr_manager:add_handler(UserId, usr_history_handler, {UserId,HistoryLimit}),
-            io:format("started user process for id ~p~n",[UserId]),
-            started;
-        _Pid ->
-            io:format("connected to user ~p~n",[UserId]),
-            ok
-    end.
-
-%%--------------------------------------------------------------------
-%% Function: terminate(UserId, Reason) -> true
-%% Description: kills the supervisor which should trickle down to the
-%% children.
-%%--------------------------------------------------------------------
-terminate(UserId, Reason) ->
-    Pid = global:whereis_name(UserId),
-    io:format("Killing user ~p. Reason: ~p~n",[UserId, Reason]),
-    erlang:exit(Pid, kill).
-
-%%--------------------------------------------------------------------
-%% Function: subscribe(UserId) -> {ok, HandlerId}
-%% Description: adds a listen handler to a gen_event and notifies the
-%% gen_fsm to track it.
-%%--------------------------------------------------------------------
-subscribe(UserId) ->
-    HandlerId = {usr_listen_handler, make_ref()},
-    usr_manager:add_sup_handler(UserId, HandlerId, {UserId, self(), HandlerId}),
-    usr_monitor:add_handler(UserId, HandlerId),
-    {ok, HandlerId}.
-
-%%--------------------------------------------------------------------
-%% Function: subscribers(UserId) -> [Subscriber]
-%% Description: returns a list of all the handlers attached to a given
-%% user.
-%%--------------------------------------------------------------------
-subscribers(UserId) ->
-    usr_manager:get_handlers(UserId).
-
-%%--------------------------------------------------------------------
-%% Function: unsubscribe(UserId, HandlerId) -> ok
-%% Description: removes a client's listen handler from a user.
-%%--------------------------------------------------------------------
-unsubscribe(UserId, HandlerId) ->
-    usr_manager:delete_handler(UserId, HandlerId, []),
-    usr_monitor:delete_handler(UserId, HandlerId),
-    ok.
-
-%%--------------------------------------------------------------------
-%% Function: message(From, To, Message) -> ok
-%% Description: Takes a message and routes it through the sending user
-%%--------------------------------------------------------------------
-message(From, To, Message) ->
-    usr_manager:notify(From, {send, To, Message}),
-    ok.
-
-%%--------------------------------------------------------------------
-%% Function: relay(From, To, Message) -> ok
-%% Description: Usually called by the dispatch handler. takes a sent
-%% message and routes it to the receiving user.
-%%--------------------------------------------------------------------
-relay(From, To, Message) ->
-    usr_manager:notify(To, {received, From, Message}),
-    ok.
-
-%%--------------------------------------------------------------------
-%% Function: history(UserId) -> [Messages]
-%% Description: Called by the client. Contacts an event handler
-%% that returns the last messages sent and received by UserId.
-%%--------------------------------------------------------------------
-history(UserId) ->
-    usr_manager:notify(UserId, {self(), history}),
-    receive
-        {UserId, {history, History}} ->
-            History
-        after 5000 ->
-            throw(no_history_response)
-    end.

src/usr_SUITE.erl

-%%%-------------------------------------------------------------------
-%%% File    : usr_SUITE.erl
-%%% Description: Basic tests for a user and its clients
-%%%
-%%% Created : 2009/12/23
-%%%-------------------------------------------------------------------
--module(usr_SUITE).
--compile(export_all).
--include("ct.hrl").
-%-include_lib("common_test/include/ct.hrl").
-%%--------------------------------------------------------------------
-%% Function: suite() -> Info
-%% Info = [tuple()]
-%%--------------------------------------------------------------------
-suite() ->
-    [{timetrap,{seconds,30}}].
-%%--------------------------------------------------------------------
-%% Function: init_per_suite(Config0) ->
-%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
-%% Config0 = Config1 = [tuple()]
-%% Reason = term()
-%%--------------------------------------------------------------------
-init_per_suite(Config) ->
-    Config.
-%%--------------------------------------------------------------------
-%% Function: end_per_suite(Config0) -> void() | {save_config,Config1}
-%% Config0 = Config1 = [tuple()]
-%%--------------------------------------------------------------------
-end_per_suite(_Config) ->
-    ok.
-%%--------------------------------------------------------------------
-%% Function: init_per_group(GroupName, Config0) ->
-%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
-%% GroupName = atom()
-%% Config0 = Config1 = [tuple()]
-%% Reason = term()
-%%--------------------------------------------------------------------
-init_per_group(_GroupName, Config) ->
-    Config.
-%%--------------------------------------------------------------------
-%% Function: end_per_group(GroupName, Config0) ->
-%%               void() | {save_config,Config1}
-%% GroupName = atom()
-%% Config0 = Config1 = [tuple()]
-%%--------------------------------------------------------------------
-end_per_group(_GroupName, _Config) ->
-    ok.
-%%--------------------------------------------------------------------
-%% Function: init_per_testcase(TestCase, Config0) ->
-%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
-%% TestCase = atom()
-%% Config0 = Config1 = [tuple()]
-%% Reason = term()
-%%--------------------------------------------------------------------
-init_per_testcase(_TestCase, Config) ->
-    Config.
-%%--------------------------------------------------------------------
-%% Function: end_per_testcase(TestCase, Config0) ->
-%%               void() | {save_config,Config1} | {fail,Reason}
-%% TestCase = atom()
-%% Config0 = Config1 = [tuple()]
-%% Reason = term()
-%%--------------------------------------------------------------------
-end_per_testcase(_TestCase, _Config) ->
-    ok.
-%%--------------------------------------------------------------------
-%% Function: groups() -> [Group]
-%% Group = {GroupName,Properties,GroupsAndTestCases}
-%% GroupName = atom()
-%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}]
-%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase]
-%% TestCase = atom()
-%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}}
-%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail |
-%%              repeat_until_any_ok | repeat_until_any_fail
-%% N = integer() | forever
-%%--------------------------------------------------------------------
-groups() ->
-    [].
-%%--------------------------------------------------------------------
-%% Function: all() -> GroupsAndTestCases | {skip,Reason}
-%% GroupsAndTestCases = [{group,GroupName} | TestCase]
-%% GroupName = atom()
-%% TestCase = atom()
-%% Reason = term()
-%%--------------------------------------------------------------------
-all() -> 
-    [usr_timeout, usr_subscribe, usr_message, usr_relay, usr_conv,
-     usr_hist,
-     client_conv, two_clients_one_usr, client_aware_crashed_user,
-     client_hist].
-
-%%--------------------------------------------------------------------
-%% Function: usr_timeout(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: tests that a user without any clients attached to it
-%% times out in due time.
-%%--------------------------------------------------------------------
-usr_timeout(_Config) ->
-    [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
-    IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
-    usr:start(Id, Sleep, 0),
-    true = lists:all(IsProc, Expected),
-    timer:sleep(Sleep*3),
-    io:format("procs: ~p~n", [global:registered_names()]),
-    false = lists:any(IsProc, Expected),
-    ok.
-
-%%--------------------------------------------------------------------
-%% Function: usr_subscribe(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Checks that a client listening to a user keeps it from
-%% timing out, and that once it stops listening, the user does time out.
-%%--------------------------------------------------------------------
-usr_subscribe(_Config) ->
-    [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
-    IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
-    usr:start(Id, Sleep, 0),
-    {ok, Handler} = usr:subscribe(Id),
-    timer:sleep(Sleep*3),
-    true = lists:all(IsProc, Expected),
-    usr:unsubscribe(Id, Handler),
-    process_flag(trap_exit, true),
-    timer:sleep(Sleep*3),
-    false = lists:any(IsProc, Expected),
-    receive
-        {'EXIT',_Pid, killed} -> ok
-    after 500 ->
-        io:format("Process won't die after the event manager did!~n")
-    end,
-    ok.
-
-%%--------------------------------------------------------------------
-%% Function: usr_message(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Tests that a client's listener notifies said client
-%% whenever a message is sent from the user the listener is attached
-%% to.
-%%--------------------------------------------------------------------
-usr_message(_Config) ->
-    usr:start(send_id, 500, 0),
-    {ok, Handler} = usr:subscribe(send_id),
-    usr:message(send_id, invalid_id, hello),
-    X = receive
-            {{send_id, Handler}, {sent, invalid_id, hello}} -> true
-        after 1000 -> throw("Message not sent")
-        end,
-    usr:unsubscribe(send_id, Handler),
-    true = X.
-
-%%--------------------------------------------------------------------
-%% Function: usr_relay(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Confirms that a client listening to a user that
-%% receives a message also receives the same message.
-%%--------------------------------------------------------------------
-usr_relay(_Config) ->
-    usr:start(sender, 500, 0),
-    {ok, HandlerS} = usr:subscribe(sender),
-    usr:start(rec, 500, 0),
-    {ok, HandlerR} = usr:subscribe(rec),
-    usr:message(sender, rec, hello),
-    X = receive
-            {{rec,HandlerR}, {received, sender, hello}} -> true
-        after 1000 -> throw("Message not received")
-        end,
-    usr:unsubscribe(sender, HandlerS),
-    usr:unsubscribe(rec, HandlerR),
-    true = X.
-
-%%--------------------------------------------------------------------
-%% Function: usr_conv(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Simulates a conversation between two different users
-%% with one client each.
-%%--------------------------------------------------------------------
-usr_conv(_Config) ->
-    {A,B} = {make_ref(), make_ref()},
-    usr:start(A, 500, 0),
-    {ok, HandlerA} = usr:subscribe(A),
-    usr:start(B, 500, 0),
-    {ok, HandlerB} = usr:subscribe(B),
-    usr:message(A,B,"Hello, B"),
-    usr:message(B,A,"Oh, Hello, A!"),
-    receive {{B,HandlerB}, {received, A, "Hello, B"}} -> ok
-    after 500 -> throw("Message not received")
-    end,
-    receive {{A,HandlerA}, {sent, B, "Hello, B"}} -> ok
-    after 500 -> throw("Message not sent")
-    end,
-    receive {{A,HandlerA}, {received, B, "Oh, Hello, A!"}} -> ok
-    after 500 -> throw("Message not received")
-    end,
-    receive {{B,HandlerB}, {sent, A, "Oh, Hello, A!"}} -> ok
-    after 500 -> throw ("Message not sent")
-    end,
-    usr:message(A,B, "Well, goodbye!"),
-    usr:message(B,A, "Have a nice day..."),
-    receive {{B,HandlerB}, {received, A, "Well, goodbye!"}} -> ok
-    after 500 -> throw("Message not received")
-    end,
-    receive {{A,HandlerA}, {sent, B, "Well, goodbye!"}} -> ok
-    after 500 -> throw("Message not sent")
-    end,
-    receive {{A,HandlerA}, {received, B, "Have a nice day..."}} -> ok
-    after 500 -> throw("Message not received")
-    end,
-    receive {{B,HandlerB}, {sent, A, "Have a nice day..."}} -> ok
-    after 500 -> throw ("Message not sent")
-    end,
-    usr:unsubscribe(A, HandlerA),
-    usr:unsubscribe(B, HandlerB).
-
-
-%%--------------------------------------------------------------------
-%% Function: usr_hist(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Simulates a short conversation and checks that a
-%% history log can be obtained.
-%%--------------------------------------------------------------------
-usr_hist(_Config) ->
-    {A,B} = {make_ref(), make_ref()},
-    usr:start(A, 500, 2),
-    {ok, HandlerA} = usr:subscribe(A),
-    usr:start(B, 500, 0),
-    {ok, HandlerB} = usr:subscribe(B),
-    [] = usr:history(A),
-    usr:message(A,B,"Hello, B"),
-    usr:message(B,A,"Oh, Hello, A!"),
-    timer:sleep(500),
-    [{sent,B,"Hello, B"},
-     {received,B,"Oh, Hello, A!"}] = usr:history(A),
-    usr:message(A,B,"Hello2, B"),
-    timer:sleep(500),
-    [{received,B,"Oh, Hello, A!"},
-     {sent,B,"Hello2, B"}] = usr:history(A),
-    usr:unsubscribe(A,HandlerA),
-    usr:unsubscribe(B,HandlerB).
-
-%%--------------------------------------------------------------------
-%% Function: client_conv(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Simulates a conversation between two different users
-%% with one client each, through the client module this time.
-%%--------------------------------------------------------------------
-client_conv(_Config) ->
-    {A,B} = {make_ref(), make_ref()},
-    {ok, ConnA} = client:connect(A),
-    {ok, ConnB} = client:connect(B),
-    client:message(A,B,"hi"),
-    [{sent, B, "hi"}] = client:listen(A,ConnA),
-    client:message(A,B, "you there?"),
-    timer:sleep(50), %% simulate delays to group messages
-    [{received, A, "hi"},{received, A, "you there?"}] = client:listen(B,ConnB),
-    client:message(B,A, "stop harassing me!"),
-    timer:sleep(50), %% simulate delays to group messages
-    [{sent, B, "you there?"},
-     {received,B, "stop harassing me!"}] = client:listen(A,ConnA),
-    [{sent, A, "stop harassing me!"}] = client:listen(B,ConnB),
-    client:disconnect(A,ConnA),
-    client:disconnect(B,ConnB).
-
-%%--------------------------------------------------------------------
-%% Function: two_clients_one_usr(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: tests that two clients can be listening to the same
-%% user within the same process.
-%%--------------------------------------------------------------------
-two_clients_one_usr(_Config) ->
-    A = make_ref(),
-    {ok, ConnA} = client:connect(A),
-    {ok, ConnB} = client:connect(A),
-    client:message(A,A,"hi"),
-    [{sent, A, "hi"},{received, A, "hi"}] = client:listen(A,ConnA),
-    [{sent, A, "hi"},{received, A, "hi"}] = client:listen(A,ConnB),
-    client:disconnect(A,ConnA),
-    client:disconnect(A,ConnB).
-
-%%--------------------------------------------------------------------
-%% Function: client_aware_crashed_user(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Checks that a client listening to a given user is
-%% notified of the user's death and exits the same way.
-%%--------------------------------------------------------------------
-client_aware_crashed_user(_Config) ->
-    A = make_ref(),
-    {ok, H} = client:connect(A),
-    usr:terminate(A,kill),
-    try client:listen(A,H) of
-        _ -> throw(no_dead_signal)
-    catch
-        exit:Reason -> Reason
-    end.
-
-%%--------------------------------------------------------------------
-%% Function: client_hist(_) ->
-%%               ok | exit() | {skip,Reason} | {comment,Comment} |
-%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
-%% Reason = term()
-%% Comment = term()
-%% Description: Checks that a client can receive the history log from
-%% the user processes. Should just be a wrapper around usr:history/1
-%%--------------------------------------------------------------------
-client_hist(_Config) ->
-    {A,B} = {make_ref(),make_ref()},
-    {ok, HA} = client:connect(A),
-    {ok, HB} = client:connect(B),
-    [] = client:history(B),
-    client:message(A,B,"hah!"),
-    client:message(A,B,"hoh!"),
-    timer:sleep(200),
-    [{sent,B,"hah!"},{sent,B,"hoh!"}] = client:history(A),
-    client:disconnect(A,HA),
-    client:disconnect(B,HB).

src/usr_dispatch_handler.erl

-%%%===================================================================
-%%% This event handler is to be attached only once per user process.
-%%% Its role is to route message from a user to another one. Without
-%%% this handler, no outbound messages can be sent.
-%%%===================================================================
--module(usr_dispatch_handler).
--compile(export_all).
--behaviour(gen_event).
--export([init/1, handle_event/2, handle_call/2, handle_info/2,
-         terminate/2, code_change/3]).
-
-%%====================================================================
-%% gen_event callbacks
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: init(UserId) -> {ok, UserId}
-%% Description: Whenever a new event handler is added to an event manager,
-%% this function is called to initialize the event handler.
-%%--------------------------------------------------------------------
-init(UserId) ->
-    {ok, UserId}.
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_event(Event, State) -> {ok, State} |
-%%                               {swap_handler, Args1, State1, Mod2, Args2} |
-%%                               remove_handler
-%% Description:Whenever an event manager receives an event sent using
-%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
-%% each installed event handler to handle the event.
-%%
-%% The only notification supported by this handler is {send, To, Msg},
-%% which triggers a message transmission to another user.
-%%--------------------------------------------------------------------
-handle_event({send, To, Msg}, UserId) ->
-    usr:relay(UserId, To, Msg),
-    {ok, UserId};
-handle_event(_Event, State) ->
-    {ok, State}.
-
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_call(Request, State) -> {ok, Reply, State} |
-%%                                {swap_handler, Reply, Args1, State1,
-%%                                  Mod2, Args2} |
-%%                                {remove_handler, Reply}
-%% Description: Whenever an event manager receives a request sent using
-%% gen_event:call/3,4, this function is called for the specified event
-%% handler to handle the request.
-%%--------------------------------------------------------------------
-handle_call(_Request, State) ->
-    Reply = ok,
-    {ok, Reply, State}.
-
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_info(Info, State) -> {ok, State} |
-%%                             {swap_handler, Args1, State1, Mod2, Args2} |
-%%                              remove_handler
-%% Description: This function is called for each installed event handler when
-%% an event manager receives any other message than an event or a synchronous
-%% request (or a system message).
-%%--------------------------------------------------------------------
-handle_info(_Info, State) ->
-  {ok, State}.
-
-%%--------------------------------------------------------------------
-%% Function: terminate(Reason, State) -> void()
-%% Description:Whenever an event handler is deleted from an event manager,
-%% this function is called. It should be the opposite of Module:init/1 and
-%% do any necessary cleaning up.
-%%--------------------------------------------------------------------
-terminate(_Reason, _State) ->
-  ok.
-
-%%--------------------------------------------------------------------
-%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
-%% Description: Convert process state when code is changed
-%%--------------------------------------------------------------------
-code_change(_OldVsn, State, _Extra) ->
-  {ok, State}.
-

src/usr_history_handler.erl

-%%%===================================================================
-%%% This event handler is to be attached once per user.
-%%% Its role is to log messages routed through a user and
-%%% message them back to a client on demand.
-%%%===================================================================
--module(usr_history_handler).
--compile(export_all).
--behaviour(gen_event).
--export([init/1, handle_event/2, handle_call/2, handle_info/2,
-         terminate/2, code_change/3]).
-
-%%====================================================================
-%% gen_event callbacks
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: init({UserId, Limit}) -> {ok, {UserId, Limit, []}}
-%% Description: Whenever a new event handler is added to an event manager,
-%% this function is called to initialize the event handler. The state
-%% is constituted of the User's Id (to couple it to messages) and
-%% a limit (how many messages need to be kept in history).
-%%--------------------------------------------------------------------
-init({UserId, Limit}) ->
-    {ok, {UserId, Limit, []}}.
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_event(Event, State) -> {ok, State} |
-%%                               {swap_handler, Args1, State1, Mod2, Args2} |
-%%                               remove_handler
-%% Description:Whenever an event manager receives an event sent using
-%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
-%% each installed event handler to handle the event.
-%%
-%% Handles two kind of messages: received and sent messages. It wraps
-%% them with a tuple containing the handler's id and the user's id
-%% so many clients can be within a single process.
-%%--------------------------------------------------------------------
-handle_event(Event={received, _, _}, {UserId, Limit, History}) ->
-    {ok, {UserId, Limit, add_to_history(Event, History, Limit)}};
-handle_event({send,To,Msg}, {UserId, Limit, History}) ->
-    Event = {sent, To, Msg},
-    {ok, {UserId, Limit, add_to_history(Event, History, Limit)}};
-handle_event({From, history}, S={UserId, _, History}) ->
-    From ! {UserId, {history, lists:reverse(History)}},
-    {ok, S};
-handle_event(_Event, State) ->
-    {ok, State}.
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_call(Request, State) -> {ok, Reply, State} |
-%%                                {swap_handler, Reply, Args1, State1,
-%%                                  Mod2, Args2} |
-%%                                {remove_handler, Reply}
-%% Description: Whenever an event manager receives a request sent using
-%% gen_event:call/3,4, this function is called for the specified event
-%% handler to handle the request.
-%%--------------------------------------------------------------------
-handle_call(_Request, State) ->
-    Reply = ok,
-    {ok, Reply, State}.
-
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_info(Info, State) -> {ok, State} |
-%%                             {swap_handler, Args1, State1, Mod2, Args2} |
-%%                              remove_handler
-%% Description: This function is called for each installed event handler when
-%% an event manager receives any other message than an event or a synchronous
-%% request (or a system message).
-%%--------------------------------------------------------------------
-handle_info(_Info, State) ->
-  {ok, State}.
-
-%%--------------------------------------------------------------------
-%% Function: terminate(Reason, State) -> void()
-%% Description:Whenever an event handler is deleted from an event manager,
-%% this function is called. It should be the opposite of Module:init/1 and
-%% do any necessary cleaning up.
-%%--------------------------------------------------------------------
-terminate(_Reason, _State) ->
-  ok.
-
-%%--------------------------------------------------------------------
-%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
-%% Description: Convert process state when code is changed
-%%--------------------------------------------------------------------
-code_change(_OldVsn, State, _Extra) ->
-  {ok, State}.
-
-%%===================================================================
-%% Internal functions
-%%===================================================================
-%%-------------------------------------------------------------------
-%% Function: add_to_history(Event, History, Limit) -> NewHistory
-%% Description: prepends a new event to the history (a list of
-%% events). If the list gets larger than the given limit, it is
-%% truncated.
-%%-------------------------------------------------------------------
-add_to_history(_, _, 0) -> [];
-add_to_history(Event, History, Limit) ->
-    case length(History) of
-        X when X >= Limit-1 ->
-            [Event|lists:sublist(History,Limit-1)];
-        _ ->
-            [Event|History]
-    end.

src/usr_listen_handler.erl

-%%%===================================================================
-%%% This event handler is to be attached by every client of a user.
-%%% Its role is to redirect messages routed through a user to its
-%%% own client.
-%%%===================================================================
--module(usr_listen_handler).
--compile(export_all).
--behaviour(gen_event).
--export([init/1, handle_event/2, handle_call/2, handle_info/2,
-         terminate/2, code_change/3]).
-
-%%====================================================================
-%% gen_event callbacks
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: init(State={UserId, SubsriberPid, HandlerId}) -> {ok, State}
-%% Description: Whenever a new event handler is added to an event manager,
-%% this function is called to initialize the event handler. The state
-%% is constituted of the User's Id (to couple it to messages), 
-%% the client's pid (to send it messages) and a unique HandlerId (to
-%% handle a client's termination and identify messages).
-%%--------------------------------------------------------------------
-init({UserId, SubscriberPid, HandlerId}) when is_pid(SubscriberPid) ->
-    {ok, {UserId, SubscriberPid, HandlerId}}.
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_event(Event, State) -> {ok, State} |
-%%                               {swap_handler, Args1, State1, Mod2, Args2} |
-%%                               remove_handler
-%% Description:Whenever an event manager receives an event sent using
-%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
-%% each installed event handler to handle the event.
-%%
-%% Handles two kind of messages: received and sent messages. It wraps
-%% them with a tuple containing the handler's id and the user's id
-%% so many clients can be within a single process.
-%%--------------------------------------------------------------------
-handle_event({received, From, Msg}, {UserId, SubscriberPid, HandlerId}) ->
-    SubscriberPid ! {{UserId, HandlerId}, {received, From, Msg}},
-    {ok, {UserId, SubscriberPid, HandlerId}};
-handle_event({send, To, Msg}, {UserId, SubscriberPid, HandlerId}) ->
-    SubscriberPid ! {{UserId, HandlerId}, {sent, To, Msg}},
-    {ok, {UserId, SubscriberPid, HandlerId}};
-handle_event(_Event, State) ->
-    {ok, State}.
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_call(Request, State) -> {ok, Reply, State} |
-%%                                {swap_handler, Reply, Args1, State1,
-%%                                  Mod2, Args2} |
-%%                                {remove_handler, Reply}
-%% Description: Whenever an event manager receives a request sent using
-%% gen_event:call/3,4, this function is called for the specified event
-%% handler to handle the request.
-%%--------------------------------------------------------------------
-handle_call(_Request, State) ->
-    Reply = ok,
-    {ok, Reply, State}.
-
-
-%%--------------------------------------------------------------------
-%% Function:
-%% handle_info(Info, State) -> {ok, State} |
-%%                             {swap_handler, Args1, State1, Mod2, Args2} |
-%%                              remove_handler
-%% Description: This function is called for each installed event handler when
-%% an event manager receives any other message than an event or a synchronous
-%% request (or a system message).
-%%--------------------------------------------------------------------
-handle_info(_Info, State) ->
-  {ok, State}.
-
-%%--------------------------------------------------------------------
-%% Function: terminate(Reason, State) -> void()
-%% Description:Whenever an event handler is deleted from an event manager,
-%% this function is called. It should be the opposite of Module:init/1 and
-%% do any necessary cleaning up.
-%%
-%% Whenever the termination reason is stop (the client is dead), the
-%% handler is removed. This is a call that's a bit ugly, because it
-%% goes straight to the fsm in order to make sure no dead handlers
-%% are counted.
-%% It also removes the link between the client and the manager to avoid
-%% killing process pools for no reason.
-%%--------------------------------------------------------------------
-terminate({stop,_}, {UserId, SubscriberPid, HandlerId}) ->
-  usr_monitor:delete_handler(UserId, HandlerId),
-  unlink(SubscriberPid),
-  ok;
-terminate(_Reason, _State) ->
-  ok.
-
-%%--------------------------------------------------------------------
-%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
-%% Description: Convert process state when code is changed
-%%--------------------------------------------------------------------
-code_change(_OldVsn, State, _Extra) ->
-  {ok, State}.

src/usr_manager.erl

-%%%===================================================================
-%%% This event manager is part of the 3 processes making a user up.
-%%% It is responsible of holding all the handlers having to do with
-%%% message handling.
-%%%===================================================================
--module(usr_manager).
-
-%% API
--export([start_link/1, add_handler/3, add_sup_handler/3, delete_handler/3,
-         notify/2, get_handlers/1]).
-
-%%--------------------------------------------------------------------
-%% Function: start_link() -> {ok,Pid} | {error,Error}
-%% Description: Creates an event manager.
-%% The user is global by default as we ought to support a multiple
-%% node system and will need more than a single process
-%%--------------------------------------------------------------------
-start_link(UserId) ->
-    gen_event:start_link({global, {manager, UserId}}).
-
-%%--------------------------------------------------------------------
-%% Function: add_handler(Module,Args) -> ok | {'EXIT',Reason} | term()
-%% Description: Adds an event handler
-%%--------------------------------------------------------------------
-add_handler(UserId, Handler = {_Module, _HandlerId}, Params) ->
-    gen_event:add_handler({global, {manager, UserId}}, Handler, Params);
-add_handler(UserId, Module, Params) when is_atom(Module) ->
-    gen_event:add_handler({global, {manager, UserId}}, Module, Params).
-
-%%--------------------------------------------------------------------
-%% Function: add_sup_handler(Module,Args) -> ok | {'EXIT',Reason} | term()
-%% Description: Adds an event handler that watches for the calling process'
-%% exit signals. This avoids having zombie listeners left over. The actual
-%% handling of errors is done in the callback module's terminate/2 function
-%%--------------------------------------------------------------------
-add_sup_handler(UserId, Handler = {_Module, _HandlerId}, Params) ->
-    gen_event:add_sup_handler({global, {manager, UserId}}, Handler, Params);
-add_sup_handler(UserId, Module, Params) when is_atom(Module) ->
-    gen_event:add_sup_handler({global, {manager, UserId}}, Module, Params).
-
-%%--------------------------------------------------------------------
-%% Function: delete_handler(UserId, Handler, Params) ->
-%% Description: Removes an event handler
-%%--------------------------------------------------------------------
-delete_handler(UserId, Handler, Params) ->
-    gen_event:delete_handler({global, {manager,UserId}}, Handler, Params).
-
-%%--------------------------------------------------------------------
-%% Function: get_handlers(UserId) -> [term()]
-%% Description: returns a list of all the handlers registered
-%%--------------------------------------------------------------------
-get_handlers(UserId) ->
-    gen_event:which_handlers({global, {manager, UserId}}).
-
-%%--------------------------------------------------------------------
-%% Function: notify(Event) -> ok | {error, Reason}
-%% Description: Sends the Event through the event manager.
-%%--------------------------------------------------------------------
-notify(UserId, Message) ->
-  gen_event:notify({global, {manager,UserId}}, Message).

src/usr_monitor.erl

-%%%===================================================================
-%%% usr_monitor is an fsm responsible for keeping the user alive when
-%%% no clients are connected. It's also responsible for a user timing
-%%% out when clients haven't connected for a given wait time.
-%%% Eventually, this might be used to monitor other users the current
-%%% user is having a conversation with so when they log off, the current
-%%% user knows.
-%%%===================================================================
--module(usr_monitor).
--behaviour(gen_fsm).
-
-%% API
--export([start_link/3, add_handler/2, delete_handler/2]).
-
-%% gen_fsm callbacks
--export([init/1, waiting/2, listening/2, handle_event/3,
-         handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
-
-%%====================================================================
-%% API
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: start_link(UserId, Dispatcher, TimeOut) ->
-%%               {ok,Pid} | ignore | {error,Error}
-%% Description:Creates a gen_fsm process which calls Module:init/1 to
-%% initialize. To ensure a synchronized start-up procedure, this function
-%% does not return until Module:init/1 has returned.
-%%
-%% In this one, TimeOut is the time to live of the user when there are
-%% no listen_handlers connected to the event manager.
-%% The Dispatcher is yet unused, but will be when monitoring of users
-%% is added in (no YAGNI rethinking needed, I hope!)
-%% The UserId is needed for the fsm to terminate its supervisor when
-%% the time is out.
-%%--------------------------------------------------------------------
-start_link(UserId, Dispatcher, TimeOut) ->
-  gen_fsm:start_link({global, {monitor,UserId}},
-                     ?MODULE,
-                     [{id, UserId},
-                      {dispatcher, Dispatcher},
-                      {timeout, TimeOut}],
-                     []).
-
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: add_handler(UserId, HandlerId) -> ok
-%% Description: Sends a message to the FSM to start tracking a new
-%% client listener.
-%%--------------------------------------------------------------------
-add_handler(UserId, HandlerId) ->
-    gen_fsm:sync_send_all_state_event({global, {monitor,UserId}}, {add_handler, HandlerId}).
-
-%%--------------------------------------------------------------------
-%% Function: delete_handler(UserId, HandlerId) -> ok
-%% Description: Orders the fsm to stop tracking the client listener.
-%% If there are no listeners left, the FSM will switch into waiting
-%% mode and timeout if there are no new handlers.
-%%--------------------------------------------------------------------
-delete_handler(UserId, HandlerId) ->
-    gen_fsm:send_event({global, {monitor,UserId}}, {delete_handler, HandlerId}).
-
-%%====================================================================
-%% gen_fsm callbacks
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: init(Args) -> {ok, StateName, State} |
-%%                         {ok, StateName, State, Timeout} |
-%%                         ignore                              |
-%%                         {stop, StopReason}
-%% Description:Whenever a gen_fsm is started using gen_fsm:start/[3,4] or
-%% gen_fsm:start_link/3,4, this function is called by the new process to
-%% initialize.
-%% Added to the state declared in start_link is an empty list of
-%% handlers.
-%%--------------------------------------------------------------------
-init(Opts) ->
-    {timeout, T} = proplists:lookup(timeout,Opts),
-    {ok, waiting, [{handlers, []} | Opts], T}.
-
-
-%%--------------------------------------------------------------------
-%% Function: waiting(timeout, State) -> {stop, normal, State}
-%% Description: the fsm waits until it times out, in which case it
-%% will kill its supervisor. Adding a handler (global event) will stop
-%% the waiting state
-%%--------------------------------------------------------------------
-waiting(timeout, State) ->
-    %% maybe store messages?
-    io:format("timing out~n"),
-    {id, UserId} = proplists:lookup(id, State),
-    usr:terminate(UserId, timeout), % take care, circular dependency!
-    {stop, normal, State};
-waiting(_Event, State) -> % catch-all to avoid crashing
-    io:format("invalid timeout msg:~p~n",[_Event]),
-    {timeout, TimeOut} = proplists:lookup(timeout, State),
-    {next_state, waiting, State, TimeOut}.
-
-%%--------------------------------------------------------------------
-%% function: listening({delete_handler, HandlerId}, State) ->
-%%              {next_state, waiting, NewState, TimeOut} |
-%%              {next_state, listening, NewState}
-%% Description: the fsm just keeps listening for the order to stop