SNAPKITTYWEST commited on
Commit
52e4cf8
·
verified ·
1 Parent(s): c62a6d6

Add src/hk_messaging_portal.erl

Browse files
Files changed (1) hide show
  1. 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).