Skip to content

Instantly share code, notes, and snippets.

@bibby
Created February 15, 2017 23:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bibby/53cecfbf3f6722c2fa2509da6c60755d to your computer and use it in GitHub Desktop.
Save bibby/53cecfbf3f6722c2fa2509da6c60755d to your computer and use it in GitHub Desktop.
ejabberd module
%% domain names have been changed for forum posting
%%
%% Ejabberd server component that relays messages sent
%% to a configured subdomain to an outside service.
%% The reponse is then relayed back to the user on another channel.
%% @author bibby <bibby@redacted.tld>
%%
%% compile with ejabberd's include/ path included.
%% erlc -pa /usr/lib/ejabberd/ebin -I /usr/lib/ejabberd/include -o /usr/lib/ejabberd/ebin mod_chatrelay.erl
%%
%% setup: modules section in ejabberd.cfg
%% { modules, [
%% .. other modules
%% {mod_chatrelay,[{host, "relay.localhost"}]}
%% ]}
%%
-module(mod_chatrelay).
-behaviour(gen_server).
-behaviour(gen_mod).
-include("ejabberd.hrl").
-include("jlib.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
%% gen_mod api
-export([
start/2,
stop/1
]).
%% gen_server api
-export([
code_change/3,
handle_call/3,
handle_cast/2,
handle_info/2,
init/1,
start_link/2,
terminate/2]
).
-export([
route/3,
respond/3,
fetchResponse/3,
strip_bom/1
]).
% captured messages are sent to this end point
-define( SERVICE_URL, "http://game.redacted.tld:3000/").
-define( SERVICE_METHOD, "rpc").
% registers as a virtual host
% messages to this channel are captured by this module
-define( ACCEPT_CHANNEL, "relay.redacted.tld" ).
% server responses are sent to this channel: ( game-{id}@muc.localhost )
-define( RETURN_CHANNEL, "global").
-define( RETURN_DOMAIN, "muc.").
%% gen_server API
start_link(Host, Opts) ->
gen_server:start_link(
{local, ?MODULE}, ?MODULE, [Host, Opts], []).
%% gen_mod API
start(Host, Opts) ->
inets:start(),
Proc = gen_mod:get_module_proc(Host, ?MODULE),
ChildSpec = {Proc,
{?MODULE, start_link, [Host, Opts]},
transient,
1000,
worker,
[?MODULE]},
supervisor:start_child(ejabberd_sup, ChildSpec).
stop(Host) ->
Proc = gen_mod:get_module_proc(Host, ?MODULE),
gen_server:call(Proc, stop),
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
init([Host, Opts]) ->
% add a new virtual host / subdomain "echo".example.com
VirtHost = gen_mod:get_opt_host(Host, Opts, ?ACCEPT_CHANNEL ),
ejabberd_router:register_route(VirtHost,{apply, ?MODULE, route}),
{ok, Host}.
handle_call(stop, _From, Host) ->
{stop, normal, ok, Host}.
handle_cast(_Msg, Host) -> {noreply, Host}.
handle_info(_Msg, Host) -> {noreply, Host}.
terminate(_Reason, Host) ->
ejabberd_router:unregister_route(Host), ok.
code_change(_OldVsn, Host, _Extra) -> {ok, Host}.
%% Helper
strip_bom([239, 187, 191 | C]) ->
C;
strip_bom(C) ->
C.
%% Custom Message Routing
route(From, To, {xmlelement, "message", _, _} = Packet) ->
?INFO_MSG("Custom routing",[]),
case
xml:get_subtag_cdata(Packet, "body")
of
"" ->
?INFO_MSG("Empty body",[]),
ok;
Body ->
case
xml:get_tag_attr_s("type", Packet)
of
"error" ->
?INFO_MSG("body with type=error",[]),
?ERROR_MSG(
"Received error message~n~p -> ~p~n~p",
[From, To, Packet]
);
_ ->
?INFO_MSG("making API call",[]),
{ok, Response} = fetchResponse(From, To, strip_bom(Body)),
?INFO_MSG("got '~p'",[Response]),
respond( From, To, Response)
end
end,
ok.
fetchResponse(From, To, BodyStr) ->
{jid,Room,_,_,_,_,_} = To,
{ok, {{_Version, 200, _ReasonPhrase}, _Headers, Body}} =
httpc:request(
post,
{
string:concat( ?SERVICE_URL, ?SERVICE_METHOD ),
[],
"application/x-www-form-urlencoded",
urlenc:encode([
{"from", jlib:jid_to_string(From) },
{"to", jlib:jid_to_string(To) },
{"body", BodyStr }
])
},
[],
[]
),
?INFO_MSG("response to ~p: ~p~n", [Room,Body]),
{ok, Body}.
getId( Jid ) ->
PosA = string:str( Jid, "/" ),
PosB = string:str( Jid, "@" ),
substr( Jid, PosA, PosB - PosA ).
substr( _Jid, 0, _PosB) ->
"";
substr( Str, PosA, PosB) ->
string:substr( Str, PosA, PosB).
getHost( Jid ) ->
PosA = string:str( Jid, "@" ),
PosB = string:str( Jid, "/" ),
substr( Jid, PosA + 1, PosB ).
chatRoom( _Room, "") ->
?RETURN_CHANNEL;
chatRoom( Room, ChatRoom ) ->
string:concat( Room, getId(ChatRoom) ).
respondPublically( _From , _To, []) ->
empty;
respondPublically( From , To, Body) ->
{jid,Room,_,_,_,_,_} = To,
Json = ejson:encode(Body),
ChatRoom = jlib:jid_to_string( To ),
ToJid = jlib:make_jid(
chatRoom( Room, getId(ChatRoom) ),
string:concat(?RETURN_DOMAIN, getHost(ChatRoom)),
""
),
XmlBody = {
xmlelement,
"message",
[
{"type", "groupchat"},
{"xmlns", "jabber:client"}
],
[
{ xmlelement,
"body",
[],
[
{xmlcdata, Json}
]
}
]
},
ejabberd_router:route( From, ToJid, XmlBody).
respondPrivately( _FromPlayer, _ToServer, []) ->
empty;
respondPrivately( FromPlayer, _ToServer, Body) ->
ToPlayer = FromPlayer,
Json = ejson:encode(Body),
FromServer = jlib:make_jid(
?RETURN_CHANNEL,
string:concat(?RETURN_DOMAIN,getHost(FromPlayer)),
""
),
XmlBody = {
xmlelement,
"message",
[
{"type", "chat"},
{"xmlns", "jabber:client"}
],
[
{ xmlelement,
"body",
[],
[
{xmlcdata, Json}
]
}
]
},
ejabberd_router:route( FromServer, ToPlayer, XmlBody).
respond( From, To, Body) ->
{jid,_,_,_,_,_,_} = To,
{[{<<"public">>,Pub},{<<"private">>,Priv}]} = ejson:decode(Body),
respondPrivately( From, To, Priv ),
respondPublically( From, To, Pub ).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment