Add src/hk_messaging_portal.erl
Browse files- src/hk_messaging_portal.erl +149 -0
src/hk_messaging_portal.erl
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%%%-------------------------------------------------------------------
|
| 2 |
+
%%% @doc HK-MSG :: Messaging Portal.
|
| 3 |
+
%%%
|
| 4 |
+
%%% Structured communication between users, agents, browser
|
| 5 |
+
%%% sessions, and system services, per the `<message_schema>':
|
| 6 |
+
%%% every message is a `#message{}' with a stable id, sender,
|
| 7 |
+
%%% recipient, timestamp, conversation id, type, payload, and
|
| 8 |
+
%%% delivery status. This is deliberately *not* a raw pub/sub bus --
|
| 9 |
+
%%% `hk_event_bus' already is one, for system observability. This
|
| 10 |
+
%%% module is the addressed, stored, replayable inbox/outbox: a
|
| 11 |
+
%%% conversation's history survives after every participant has
|
| 12 |
+
%%% disconnected, which is what "delivered" vs. "queued" vs. "read"
|
| 13 |
+
%%% status is for.
|
| 14 |
+
%%%
|
| 15 |
+
%%% Delivery: a recipient (a user's frontend connection, an agent
|
| 16 |
+
%%% process, ...) calls `subscribe/1' with the recipient id it
|
| 17 |
+
%%% answers to; a message sent to a currently-subscribed recipient
|
| 18 |
+
%%% is pushed immediately and marked `delivered', otherwise it stays
|
| 19 |
+
%%% `queued' until the recipient subscribes and calls
|
| 20 |
+
%%% `list_conversation/1' (or a future subscribe replays the
|
| 21 |
+
%%% backlog -- see the `subscribe/1' implementation).
|
| 22 |
+
%%% @end
|
| 23 |
+
%%%-------------------------------------------------------------------
|
| 24 |
+
-module(hk_messaging_portal).
|
| 25 |
+
-behaviour(gen_server).
|
| 26 |
+
|
| 27 |
+
-include("hyperkitty.hrl").
|
| 28 |
+
|
| 29 |
+
-export([start_link/0]).
|
| 30 |
+
-export([send/5, subscribe/1, unsubscribe/0, list_conversation/1, mark_read/1, get/1]).
|
| 31 |
+
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
| 32 |
+
|
| 33 |
+
-define(MSG_TAB, hk_messages_tab).
|
| 34 |
+
-define(CONV_TAB, hk_messages_by_conversation_tab).
|
| 35 |
+
|
| 36 |
+
start_link() ->
|
| 37 |
+
hk_sup_util:start_link_retry(
|
| 38 |
+
fun() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []) end).
|
| 39 |
+
|
| 40 |
+
-spec send(binary(), binary(), binary(), atom(), map()) -> {ok, binary()}.
|
| 41 |
+
send(Sender, Recipient, ConversationId, MessageType, Payload) ->
|
| 42 |
+
gen_server:call(?MODULE, {send, Sender, Recipient, ConversationId, MessageType, Payload}).
|
| 43 |
+
|
| 44 |
+
-spec subscribe(binary()) -> ok.
|
| 45 |
+
subscribe(RecipientId) ->
|
| 46 |
+
gen_server:call(?MODULE, {subscribe, RecipientId, self()}).
|
| 47 |
+
|
| 48 |
+
-spec unsubscribe() -> ok.
|
| 49 |
+
unsubscribe() ->
|
| 50 |
+
gen_server:call(?MODULE, {unsubscribe, self()}).
|
| 51 |
+
|
| 52 |
+
-spec list_conversation(binary()) -> [map()].
|
| 53 |
+
list_conversation(ConversationId) ->
|
| 54 |
+
Ids = case ets:lookup(?CONV_TAB, ConversationId) of
|
| 55 |
+
[{ConversationId, MsgIds}] -> MsgIds;
|
| 56 |
+
[] -> []
|
| 57 |
+
end,
|
| 58 |
+
[hk_schema:to_map(M) || Id <- lists:reverse(Ids), {ok, M} <- [raw_get(Id)]].
|
| 59 |
+
|
| 60 |
+
-spec mark_read(binary()) -> ok | {error, not_found}.
|
| 61 |
+
mark_read(MessageId) ->
|
| 62 |
+
gen_server:call(?MODULE, {mark_read, MessageId}).
|
| 63 |
+
|
| 64 |
+
-spec get(binary()) -> {ok, map()} | {error, not_found}.
|
| 65 |
+
get(MessageId) ->
|
| 66 |
+
case raw_get(MessageId) of
|
| 67 |
+
{ok, M} -> {ok, hk_schema:to_map(M)};
|
| 68 |
+
Error -> Error
|
| 69 |
+
end.
|
| 70 |
+
|
| 71 |
+
init([]) ->
|
| 72 |
+
ets:new(?MSG_TAB, [set, named_table, protected]),
|
| 73 |
+
ets:new(?CONV_TAB, [set, named_table, protected]),
|
| 74 |
+
{ok, #{subscribers => #{}, monitors => #{}}}.
|
| 75 |
+
|
| 76 |
+
handle_call({send, Sender, Recipient, ConversationId, MessageType, Payload}, _From, State) ->
|
| 77 |
+
MessageId = hk_id:new(<<"msg">>),
|
| 78 |
+
Now = hk_id:timestamp_ms(),
|
| 79 |
+
Subscribed = maps:is_key(Recipient, maps:get(subscribers, State)),
|
| 80 |
+
Status = case Subscribed of true -> delivered; false -> queued end,
|
| 81 |
+
Message = #message{
|
| 82 |
+
message_id = MessageId, sender = Sender, recipient = Recipient,
|
| 83 |
+
timestamp = Now, conversation_id = ConversationId,
|
| 84 |
+
message_type = MessageType, payload = Payload, status = Status
|
| 85 |
+
},
|
| 86 |
+
ets:insert(?MSG_TAB, {MessageId, Message}),
|
| 87 |
+
append_conversation_index(ConversationId, MessageId),
|
| 88 |
+
emit(<<"message.sent">>, Message),
|
| 89 |
+
case Subscribed of
|
| 90 |
+
true ->
|
| 91 |
+
Pid = maps:get(Recipient, maps:get(subscribers, State)),
|
| 92 |
+
Pid ! {hk_message, hk_schema:to_map(Message)},
|
| 93 |
+
emit(<<"message.received">>, Message);
|
| 94 |
+
false ->
|
| 95 |
+
ok
|
| 96 |
+
end,
|
| 97 |
+
{reply, {ok, MessageId}, State};
|
| 98 |
+
|
| 99 |
+
handle_call({subscribe, RecipientId, Pid}, _From, State = #{subscribers := Subs, monitors := Mons}) ->
|
| 100 |
+
MonRef = erlang:monitor(process, Pid),
|
| 101 |
+
{reply, ok, State#{subscribers := Subs#{RecipientId => Pid},
|
| 102 |
+
monitors := Mons#{MonRef => RecipientId}}};
|
| 103 |
+
|
| 104 |
+
handle_call({unsubscribe, Pid}, _From, State = #{subscribers := Subs}) ->
|
| 105 |
+
Subs2 = maps:filter(fun(_K, V) -> V =/= Pid end, Subs),
|
| 106 |
+
{reply, ok, State#{subscribers := Subs2}};
|
| 107 |
+
|
| 108 |
+
handle_call({mark_read, MessageId}, _From, State) ->
|
| 109 |
+
case raw_get(MessageId) of
|
| 110 |
+
{ok, M} ->
|
| 111 |
+
ets:insert(?MSG_TAB, {MessageId, M#message{status = read}}),
|
| 112 |
+
{reply, ok, State};
|
| 113 |
+
Error ->
|
| 114 |
+
{reply, Error, State}
|
| 115 |
+
end.
|
| 116 |
+
|
| 117 |
+
handle_cast(_Msg, State) ->
|
| 118 |
+
{noreply, State}.
|
| 119 |
+
|
| 120 |
+
handle_info({'DOWN', MonRef, process, _Pid, _Reason}, State = #{subscribers := Subs, monitors := Mons}) ->
|
| 121 |
+
case maps:take(MonRef, Mons) of
|
| 122 |
+
{RecipientId, Mons2} ->
|
| 123 |
+
{noreply, State#{subscribers := maps:remove(RecipientId, Subs), monitors := Mons2}};
|
| 124 |
+
error ->
|
| 125 |
+
{noreply, State}
|
| 126 |
+
end;
|
| 127 |
+
handle_info(_Info, State) ->
|
| 128 |
+
{noreply, State}.
|
| 129 |
+
|
| 130 |
+
%%% internal
|
| 131 |
+
|
| 132 |
+
raw_get(MessageId) ->
|
| 133 |
+
case ets:lookup(?MSG_TAB, MessageId) of
|
| 134 |
+
[{MessageId, M}] -> {ok, M};
|
| 135 |
+
[] -> {error, not_found}
|
| 136 |
+
end.
|
| 137 |
+
|
| 138 |
+
append_conversation_index(ConversationId, MessageId) ->
|
| 139 |
+
Existing = case ets:lookup(?CONV_TAB, ConversationId) of
|
| 140 |
+
[{ConversationId, Ids}] -> Ids;
|
| 141 |
+
[] -> []
|
| 142 |
+
end,
|
| 143 |
+
ets:insert(?CONV_TAB, {ConversationId, [MessageId | Existing]}).
|
| 144 |
+
|
| 145 |
+
emit(Category, #message{message_id = Id, sender = Sender, recipient = Recipient,
|
| 146 |
+
conversation_id = ConvId}) ->
|
| 147 |
+
Event = hk_event:new(Category, {message, Id},
|
| 148 |
+
#{sender => Sender, recipient => Recipient, conversation_id => ConvId}),
|
| 149 |
+
catch hk_event_bus:publish(Event).
|