diff --git a/mod_post_log/Emakefile b/mod_post_log/Emakefile new file mode 100644 index 0000000..60bb572 --- /dev/null +++ b/mod_post_log/Emakefile @@ -0,0 +1,2 @@ +{'../ejabberd-dev/src/gen_mod', [{outdir, "../ejabberd-dev/ebin"},{i,"../ejabberd-dev/include"}]}. +{'src/mod_post_log', [{outdir, "ebin"},{i,"../ejabberd-dev/include"}]}. diff --git a/mod_post_log/build.sh b/mod_post_log/build.sh new file mode 100755 index 0000000..f0a0cfa --- /dev/null +++ b/mod_post_log/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -pa ../ejabberd-dev/ebin -pz ebin -make diff --git a/mod_post_log/ebin/.keepme b/mod_post_log/ebin/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/mod_post_log/src/mod_post_log.erl b/mod_post_log/src/mod_post_log.erl new file mode 100644 index 0000000..cfafffb --- /dev/null +++ b/mod_post_log/src/mod_post_log.erl @@ -0,0 +1,157 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_post_log.erl +%%% Author : Tim Stewart +%%% Purpose : POST user messages to server via HTTP +%%% Created : 02 Aug 2014 by Tim Stewart +%%% +%%% Based on mod_service_log.erl +%%%---------------------------------------------------------------------- + +-module(mod_post_log). +-author('tim@stoo.org'). + +-behaviour(gen_mod). + +-export([start/2, + stop/1, + log_user_send/3, + post_result/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +start(Host, _Opts) -> + ok = case inets:start() of + {error, {already_started, inets}} -> + ok; + ok -> + ok + end, + ejabberd_hooks:add(user_send_packet, Host, + ?MODULE, log_user_send, 50), + ok. + +stop(Host) -> + ejabberd_hooks:delete(user_send_packet, Host, + ?MODULE, log_user_send, 50), + ok. + +log_user_send(From, To, Packet) -> + ok = log_packet(From, To, Packet). + +log_packet(From, To, {xmlelement, "message", _Attrs, _Els} = Packet) -> + ok = log_message(From, To, Packet); + +log_packet(_From, _To, {xmlelement, _Name, _Attrs, _Els}) -> + ok. + +log_message(From, To, {xmlelement, _Name, Attrs, _Els} = Packet) -> + Type = lists:keyfind("type", 1, Attrs), + log_message_filter(Type, From, To, Packet). + +log_message_filter({"type", Type}, From, To, Packet) + when Type =:= "chat"; + Type =:= "groupchat" -> + log_chat(From, To, Packet); +log_message_filter(_Other, _From, _To, _Packet) -> + ok. + +log_chat(From, To, {xmlelement, _Name, _Attrs, Els} = Packet) -> + case get_body(Els) of + no_body -> + ok; + {ok, _Body} -> + log_chat_with_body(From, To, Packet) + end. + +log_chat_with_body(From, To, Packet) -> + post_xml(From, To, xml:element_to_binary(Packet)). + +post_xml(From, To, Xml) -> + Ts = to_iso_8601_date(os:timestamp()), + + Body = Xml, + + Url = get_opt(url), + TsHeader = get_opt(ts_header, "X-Message-Timestamp"), + FromHeader = get_opt(from_header, "X-Message-From"), + ToHeader = get_opt(to_header, "X-Message-To"), + Headers = [ {TsHeader, Ts}, + {FromHeader, format_jid(From)}, + {ToHeader, format_jid(To)} + | get_opt(headers, []) ], + ContentType = get_opt(content_type, "text/xml"), + HttpOptions = get_opt(http_options, []), + ReqOptions = get_opt(req_options, []), + + {ok, _ReqId} = httpc:request(post, + {Url, Headers, ContentType, Body}, + HttpOptions, + [ {sync, false}, + {receiver, {?MODULE, post_result, []}} + | ReqOptions ]), + ok. + +post_result({_ReqId, {error, Reason}}) -> + report_error([ {error, Reason } ]); +post_result({_ReqId, Result}) -> + {StatusLine, Headers, Body} = Result, + {_HttpVersion, StatusCode, ReasonPhrase} = StatusLine, + if StatusCode < 200; + StatusCode > 299 -> + ok = report_error([ {status_code, StatusCode}, + {reason_phrase, ReasonPhrase}, + {headers, Headers}, + {body, Body} ]), + ok; + true -> + ok + end. + +get_body(Els) -> + XmlElements = [ El || El <- Els, element(1, El) =:= xmlelement ], + case lists:keyfind("body", 2, XmlElements) of + false -> + no_body; + {xmlelement, "body", _, InnerEls} -> + case lists:keyfind(xmlcdata, 1, InnerEls) of + false -> + no_body; + {xmlcdata, Body} -> + {ok, Body} + end + end. + +get_opt(Opt) -> + get_opt(Opt, undefined). + +get_opt(Opt, Default) -> + gen_mod:get_module_opt(global, ?MODULE, Opt, Default). + +report_error(ReportArgs) -> + ok = error_logger:error_report([ mod_post_log_cannot_post | ReportArgs ]). + +format_jid(#jid{luser = User, lserver = Server, lresource = Resource}) + when Resource =:= undefined; + Resource =:= ""; + Resource =:= <<"">> -> + %% The guard above feels defensive, but I don't yet know the full + %% set of ways that ejabberd will represent an empty resource + io_lib:format("~s@~s", [User, Server]); +format_jid(#jid{luser = User, lserver = Server, lresource = Resource}) -> + io_lib:format("~s@~s/~s", [User, Server, Resource]). + +%% Erlang now()-style timestamps are in UTC by definition, and we are +%% assuming ISO 8601 dates should be printed in UTC as well, so no +%% conversion necessary +%% +%% Example: +%% {1385,388790,334905} +%% -becomes- +%% 2013-11-25 14:13:10.334905Z +-spec to_iso_8601_date(erlang:timestamp()) -> string(). +to_iso_8601_date(Timestamp) when is_tuple(Timestamp) -> + {{Y, Mo, D}, {H, M, S}} = calendar:now_to_universal_time(Timestamp), + {_, _, US} = Timestamp, + lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B.~6.10.0BZ", + [Y, Mo, D, H, M, S, US])).