%%%---------------------------------------------------------------------- %%% File : mod_archive_webview.erl %%% Author : Olivier Goffart %%% Purpose : Online viewer of message archive. (to be used with mod_archive_odbc) %%% Created : %%% Id : %%%---------------------------------------------------------------------- -module(mod_archive_webview). -author('ogoffart@kde.org'). -export([ process/2 ]). -include("ejabberd.hrl"). -include("jlib.hrl"). -include("web/ejabberd_http.hrl"). -include("web/ejabberd_web_admin.hrl"). %for all the defines -define(LINK(L) , "/archive/" ++ L). %-define(P(Els), ?XE("p", Els)). -define(PC(Text), ?XE("p", [?C(Text)])). -define(PCT(Text), ?PC(?T(Text))). -define(MYDEBUG(Format, Args), io:format("D(~p:~p:~p) : " ++ Format ++ "~n", [calendar:local_time(), ?MODULE, ?LINE] ++ Args)). %%%---------------------------------------------------------------------- %%% REQUEST HANDLERS %%%---------------------------------------------------------------------- %process([], Request) -> process2([], Request, {}). process(["style.css"], _) -> {200,[{"Content-Type", "text/css"}], " #navigation li { list-style:none; display:inline; } .message_from, .message_to { margin:0; padding:0; } .message_from .time { color:#BD2134; font-weight:bold; } .message_from .jid { color:#BD2134; font-weight:bold; } .message_to .time { color:#1E6CC6; font-weight:bold; } .message_to .jid { color:#1E6CC6; font-weight:bold; } .search_result a { display:block; } .search_result em { display:block; color:green; } /*a.link_prev { float:left } */ a.link_next { float:right } .message_body { white-space: pre-wrap; } "}; process(Path, #request{auth = Auth} = Request) -> ?MYDEBUG("Requested ~p ~p", [Path, Request]), case get_auth(Auth) of {User, Server} -> process2(Path, Request, {User, Server}); unauthorized -> {401, [{"WWW-Authenticate", "basic realm=\"ejabberd-archives\""}], ejabberd_web:make_xhtml([{xmlelement, "h1", [], [{xmlcdata, "401 Unauthorized"}]}])} end. %process2(["config" | tail], #request{lang = Lang } = Request , {User, Server}) -> process2(["config" ], #request{lang = Lang } = _Request , {User, Server}) -> make_xhtml(?T("Config"), [?XE("h3", [?CT("Global Settings")]) ] ++ global_config_form({User, Server}, Lang) ++ [?XE("h3", [?CT("Specific Contact Settings")]) ] ++ contact_config_form({User, Server}, Lang) ++ [?X("hr"), ?ACT(?LINK("config/complex"),"Advanced settings")] , Lang); process2(["config" , "submit", "global"], #request{q = Query } = Request , US) -> submit_config_global( Query , US), process2(["config"], Request, US); % process2(["config" , "submit", "contact"], #request{q = Query } = Request , US) -> % submit_config_contact( Query , US), % process2(["config"], Request, US); process2(["config" , "complex" ], #request{lang = Lang } = _Request , {User, Server}) -> make_xhtml(?T("Config"), [?XE("h3", [?CT("Global Settings")]) ] ++ global_config_form_complex({User, Server}, Lang) ++ [?XE("h3", [?CT("Specific Contact Settings")]) ] ++ contact_config_form({User, Server}, Lang) ++ [?X("hr"), ?ACT(?LINK("config"),"Simple settings")] , Lang); process2(["config" , "submit", "complex_global"], #request{q = Query } = Request , US) -> submit_config_global_complex( Query , US), process2(["config/complex"], Request, US); process2(["contact"], #request{lang = Lang } = _Request , US) -> make_xhtml(?T("Contact List"), [ ?XE("ul", lists:map( fun({Node,Server,Count}) -> With = jlib:jid_to_string({Node,Server,""}), ?LI([?AC(?LINK("contact/" ++ ejabberd_http:url_encode(With)), With ) , ?C(" (" ++ Count ++")")] ) end, get_contacts(US))) ], Lang); process2(["contact" , Jid], #request{lang = Lang } = _Request , US) -> make_xhtml(?T("Chat with ") ++ Jid, contact_config(Jid,US,Lang) ++ [?XE("ul", lists:map( fun({Id, Node, Server, Resource, Utc, Subject }) -> With = jlib:jid_to_string({Node,Server,Resource}), ?LI([?AC(?LINK("show/" ++ integer_to_list(Id)), "On " ++ Utc ++ " with " ++ With ++ " -> " ++ escape_str(Subject) )] ) end, get_collection_list(jlib:string_to_jid(Jid), US))) ], Lang); process2(["show" , Id], #request{lang = Lang } = _Request , US) -> { With, Utc, Subject, List, NPId } = get_collection(Id, US), [Date, _Time] = string:tokens(Utc, " "), make_xhtml(?T("Chat with ") ++ jlib:jid_to_string(With) ++ ?T(" on ") ++ Date ++ ?T(" : ") ++ escape_str(Subject), lists:map(fun(Msg) -> format_message(Msg,With, US) end, List) ++ links_previous_next(NPId, Lang) ++ [?X("hr"), ?XAE("form",[{"action",?LINK("edit/" ++ Id)},{"metohd","post"}], [?XE("label",[?CT("Edit subject: "), ?INPUT("text","subject",escape_str(Subject))]), ?INPUT("submit","submit",?T("Ok"))]), ?XAE("form",[{"action",?LINK("delete/" ++ Id)},{"metohd","post"}, {"onsubmit","return confirm('"++ ?T("Do you realy want to delete this chat") ++"')"}], [?INPUT("hidden","id",Id),?INPUT("submit","delete",?T("Delete"))])] , Lang); process2(["edit" , Id], #request{ q = Query} = Request , US) -> case lists:keysearch("subject", 1, Query) of {value, {_, Subject}} -> change_subject(Id,Subject,US); _ -> ok end, process2(["show", Id] , Request, US); process2(["delete" , Id], #request{q = Query, lang=Lang} = _Request , US) -> case lists:keysearch("id", 1, Query) of {value, {_, Id2}} when Id==Id2 -> delete_collection(Id,US), make_xhtml("Chat deleted",[],Lang); _ -> ?ERR_INTERNAL_SERVER_ERROR end; process2(["search"], #request{lang = Lang } = Request , US) -> make_xhtml(?T("Search"), [ search_form(Request, US) ], Lang); process2(["search", "results"], #request{lang = Lang } = Request , US) -> make_xhtml(?T("Search"), [ search_form(Request, US) | search_results(Request, US)], Lang); process2([], #request{lang = Lang } = _Request , {LUser,LServer}) -> make_xhtml(?T("Archives viewer"),[?PCT("Welcome " ++ LUser ++ "@" ++ LServer)], Lang); process2(_, #request{lang = Lang } = _Request , _US) -> make_xhtml(?T("404 File not found"),[], Lang). %------------------------------ make_xhtml(Title, Els, Lang) -> {200, [html], {xmlelement, "html", [{"xmlns", "http://www.w3.org/1999/xhtml"}, {"xml:lang", Lang}, {"lang", Lang}], [{xmlelement, "head", [], [?XE("title", [?C(Title) , ?CT(" - ejabberd Web Archive Viewer")]), {xmlelement, "meta", [{"http-equiv", "Content-Type"}, {"content", "text/html; charset=utf-8"}], []}, {xmlelement, "link", [{"href", ?LINK("style.css")}, {"type", "text/css"}, {"rel", "stylesheet"}], []}]}, ?XE("body", [?XAE("div", [{"id", "container"}], [?XAE("div", [{"id", "header"}], [?XE("h1", [?CT("Archives Viewer")])]), ?XAE("div", [{"id", "navigation"}], [?XE("ul", [?LI([?ACT(?LINK("config"), "Config")]), ?C(" "), ?LI([?ACT(?LINK("contact"), "Browse")]), ?C(" "), ?LI([?ACT(?LINK("search"), "Search")])])]), ?C(" "), ?XAE("div", [{"id", "content"}], [ ?XE("h2", [?C(Title)]) | Els])])])]}}. get_auth(Auth) -> case Auth of {SJID, P} -> case jlib:string_to_jid(SJID) of error -> unauthorized; #jid{user = U, server = S} -> case ejabberd_auth:check_password(U, S, P) of true -> {U, S}; false -> unauthorized end end; _ -> unauthorized end. select_element(Name, List, Value1) -> Value = if is_integer(Value1) -> integer_to_list(Value1); true -> Value1 end, ?XAE("select",[{"name",Name}],lists:map( fun({Key,Text}) -> ?XAE("option", case Key of Value -> [{"value",Value},{"selected","selected"}]; _ -> [{"value",Key}] end, [?C(Text)]) end, List)). table_element(Rows) -> ?XE("table",lists:map(fun(Cols)-> ?XE("tr", lists:map(fun(Ct)-> ?XE("td",Ct) end, Cols)) end, Rows)). %------------------------ format_message({ Utc, Dir, Body } ,{WithU,WithS,WithR}, {LUser,LServer} ) -> {From, Class} = case Dir of 0 -> { jlib:jid_to_string({WithU,WithS,WithR}) , "message_from" } ; 1 -> { jlib:jid_to_string({LUser,LServer,""}) , "message_to" } end, [_Date, Time] = string:tokens(Utc, " "), ?XAE("p", [{"class", Class}] , [ ?XAE("span", [{"class","time"}], [?C("["++Time++"]")]), ?C(" "), ?XAE("span", [{"class","jid"}], [?C(From)]), ?C(": "), ?XAE("span", [{"class","message_body"}], [?C(Body)])]). contact_config(Jid,{LUser,LServer},Lang) -> %run_sql_transaction(LServer, fun() -> run_sql_query("") end) %[?XE("p",[?CT("Automatic archive with this contact is " + Au ), ]. []. global_config_form({LUser,LServer},Lang) -> {Save,Expire,Auto_save} = case run_sql_transaction(LServer, fun() -> run_sql_query( "SELECT save,expire,auto_save" " FROM archive_global_prefs" " WHERE us = " ++ get_us_escaped({LUser,LServer}) ) end) of {selected, _ , [ Ok ]} -> Ok; {selected, _ , [ ]} -> { -1, -1, -1 } end, [?XAE("form",[{"action",?LINK("config/submit/global")}], [?XE("label",[?CT("Disable or enable automatic archiving globaly: "), select_element("global_auto_save", [{"-1",?T("--Server Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Auto_save))]), ?BR, ?XE("label",[?CT("Default for contact not specified bellow : "), select_element("global_save", [{"-1",?T("--Server Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Save))]), ?BR, ?XE("label",[?CT("Default expiration time: "), ?INPUT("text","global_expire",integer_to_list(decode_integer(Expire)))]), ?CT("(number of seconds before deleting message, '-1' = server default)"), ?BR, ?INPUTT("submit","global_submit","Submit")] )]. global_config_form_complex({LUser,LServer},Lang) -> {Save,Expire,Otr,Method_auto,Method_local,Method_manual,Auto_save} = case run_sql_transaction(LServer, fun() -> run_sql_query( "SELECT save,expire,otr,method_auto,method_local,method_manual,auto_save" " FROM archive_global_prefs" " WHERE us = " ++ get_us_escaped({LUser,LServer}) ) end) of {selected, _ , [ Ok ]} -> Ok; {selected, _ , [ ]} -> { -1, -1, -1, -1, -1, -1, -1 } end, MethodList = [ {"-1",?T("--Undefined--")}, {"0",?T("Prefer")}, {"1",?T("Concede")}, {"2",?T("Forbid")} ], [?XAE("form",[{"action",?LINK("config/submit/complex_global")}],[table_element([[ [?XE("label",[?CT("Save: "), select_element("global_save",[{"-1",?T("--Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Save))])], [?XE("label",[?CT("Expire: "), ?INPUT("text","global_expire",integer_to_list(decode_integer(Expire)))])], [?XE("label",[?CT("Otr: "), select_element("global_otr",[{"-1",?T("--Undefined--")}, {"0",?T("Approve")}, {"1",?T("Concede")}, {"2",?T("Forbid")}, {"3",?T("Oppose")}, {"4",?T("Prefer")}, {"5",?T("Require")} ],decode_integer(Otr))])], [?XE("label",[?CT("Auto Method: "), select_element("global_method_auto", MethodList,decode_integer(Method_auto))])], [?XE("label",[?CT("Local Method: "), select_element("global_method_local", MethodList,decode_integer(Method_local))])], [?XE("label",[?CT("Manual Method: "), select_element("global_method_manual", MethodList,decode_integer(Method_manual))])], [?XE("label",[?CT("Auto Save "), select_element("global_auto_save", [{"-1",?T("--Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],decode_integer(Auto_save))])], [?INPUT("submit","global_modify",?T("Modify"))] ]])])]. contact_config_form({LUser,LServer},Lang) -> {selected, _, List} = run_sql_transaction(LServer, fun() -> run_sql_query( "SELECT with_user,with_server,with_resource,save,expire" " FROM archive_jid_prefs" " WHERE us = " ++ get_us_escaped({LUser,LServer}) ) end), [ table_element([[[?CT("JID")],[?CT("Auto archive")],[?CT("Expire")]] | lists:map(fun({WithU,WithS,WithR,Save,Expire}) -> [ [?C(jlib:jid_to_string({WithU,WithS,WithR}))], [case decode_integer(Save) of 1 -> ?CT("Enabled"); 0 -> ?CT("Disabled"); _ -> ?CT("Default") end], [?C(integer_to_list(decode_integer(Expire)))] ] end , List ) ]) %, % ?XAE("form",[{"action",?LINK("config/submit/contact")}], % [?XE("label",[?CT("Add/Modify settings for Jid: "), ?INPUT("text","jid","")]), % ?BR, % ?XE("label",[?CT("Archiving : "), select_element("save", % [{"-1",?T("--Default--")},{"1",?T("Enabled")},{"0",?T("Disabled")}],"-1")]), % ?BR, ?XE("label",[?CT("Expiration time: "), ?INPUT("text","expire","-1")]), % ?BR, ?INPUTT("submit","submit","Submit")] % ) ]. get_from_query_escaped(Key,Query) -> {value, {_, Value}} = lists:keysearch(Key, 1, Query), case Value of -1 -> "NULL"; Integer when is_integer(Integer) -> Integer; "-1" -> "NULL"; Value -> "'" ++ ejabberd_odbc:escape(Value) ++ "'" end. submit_config_global(Query , {LUser,LServer}) -> SUS = get_us_escaped({LUser,LServer}), SQLQuery = "UPDATE archive_global_prefs" " SET save = " ++ get_from_query_escaped("global_save",Query) ++ "," " expire = " ++ get_from_query_escaped("global_expire",Query) ++ "," " auto_save = " ++ get_from_query_escaped("global_auto_save",Query) ++ " WHERE us = " ++ SUS, F = fun() -> %TODO: use REPLACE case run_sql_query("SELECT us FROM archive_global_prefs WHERE us = " ++ SUS) of {selected, _, Rs} when Rs /= [] -> ok; _ -> run_sql_query("INSERT INTO archive_global_prefs (us) VALUES (" ++ SUS ++ ")") end, run_sql_query(SQLQuery) end, run_sql_transaction(LServer, F). submit_config_global_complex(Query , {LUser,LServer}) -> SUS = get_us_escaped({LUser,LServer}), SQLQuery = "UPDATE archive_global_prefs" " SET save = " ++ get_from_query_escaped("global_save",Query) ++ "," " expire = " ++ get_from_query_escaped("global_expire",Query) ++ "," " otr = " ++ get_from_query_escaped("global_otr",Query) ++ "," " method_auto = " ++ get_from_query_escaped("global_method_auto",Query) ++ "," " method_local = " ++ get_from_query_escaped("global_method_local",Query) ++ "," " method_manual = " ++ get_from_query_escaped("global_method_manual",Query) ++ "," " auto_save = " ++ get_from_query_escaped("global_auto_save",Query) ++ " WHERE us = " ++ SUS, F = fun() -> %TODO: use REPLACE case run_sql_query("SELECT us FROM archive_global_prefs WHERE us = " ++ SUS) of {selected, _, Rs} when Rs /= [] -> ok; _ -> run_sql_query("INSERT INTO archive_global_prefs (us) VALUES (" ++ SUS ++ ")") end, run_sql_query(SQLQuery) end, run_sql_transaction(LServer, F). get_from_query_with_default(Key,Query,Default) -> case lists:keysearch(Key, 1, Query) of {value, {_, Value}} -> Value; _ -> Default end. search_form( #request{lang = Lang, q = Query } = _Request, US) -> ?XAE("form",[{"method","post"},{"action", ?LINK("search/results")}], [ ?XE("label", [?CT("With: ") , select_element("with" , [ { "", "--All--" } | lists:map( fun({Node,Server,_Count}) -> With = jlib:jid_to_string({Node,Server,""}), {With, With} end, get_contacts(US)) ], get_from_query_with_default("with",Query,""))]), ?BR, ?XE("label", [?CT("From: ") , ?INPUT("text","from", get_from_query_with_default("from",Query,"")), ?CT(" (date in SQL format, may be empty)")]), ?BR, ?XE("label", [?CT("To: ") , ?INPUT("text","to", get_from_query_with_default("to",Query,"")), ?CT(" (date in SQL format, may be empty)")]), ?BR, ?XE("label", [?CT("Search keyword: ") , ?INPUT("text","keywords", get_from_query_with_default("keywords",Query,""))]), ?BR, ?INPUT("submit","search",?T("Search")) ]). search_results( #request{lang = Lang, q = Query } = _Request, {_, LServer} = US) -> With = case lists:keysearch("with", 1, Query) of {value, {_, Value}} -> case jlib:string_to_jid(Value) of #jid{ luser = Node , lserver = Server , lresource = "" } -> " AND with_user ='" ++ ejabberd_odbc:escape(Node) ++ "'" ++ " AND with_server ='" ++ ejabberd_odbc:escape(Server) ++ "'"; #jid{ luser = Node , lserver = Server , lresource = Resource } -> " AND with_user ='" ++ ejabberd_odbc:escape(Node) ++ "'" ++ " AND with_server ='" ++ ejabberd_odbc:escape(Server) ++ "'" ++ " AND with_resource ='" ++ ejabberd_odbc:escape(Resource) ++ "'"; _ -> "" end; _ -> "" end, From = case lists:keysearch("from", 1, Query) of {value, {_, V1}} when V1 /= "" -> " AND M.utc >= '" ++ ejabberd_odbc:escape(V1) ++ "'"; _ -> "" end, To = case lists:keysearch("to", 1, Query) of {value, {_, V2}} when V2 /= "" -> " AND M.utc <= '" ++ ejabberd_odbc:escape(V2) ++ "'"; _ -> "" end, Kw = case lists:keysearch("keywords", 1, Query) of {value, {_, V3}} when V3 /= "" -> " AND body LIKE '%" ++ ejabberd_odbc:escape(V3) ++ "%'"; _ -> "" end, F = fun() -> run_sql_query( "SELECT coll_id,subject,with_user,with_server,with_resource,C.utc,body" " FROM archive_collections as C, archive_messages as M" " WHERE C.id = M.coll_id AND C.us = " ++ get_us_escaped(US) ++ " AND C.deleted='0'" ++ With ++ From ++ To ++ Kw ++ " GROUP BY coll_id") end, case run_sql_transaction(LServer,F) of {selected, _ , []} -> [?PCT("No matches")]; {selected, _ , Results} -> lists:map(fun(R) -> format_search_result(R,Lang) end, Results) end. format_search_result( {Id,Subject,User,Server,Resource,Utc,Body} ,_Lang) -> ?XAE("p",[{"class","search_result"}], [?AC(?LINK("show/" ++ integer_to_list(Id)), jlib:jid_to_string({User,Server,Resource}) ++ " : " ++ escape_str(Subject)), ?C(Body), ?XE("em",[?C(Utc)]) ] ). links_previous_next({PrevId,NextId},Lang) -> [?XAE("p",[{"class","links_previous_next"}], links_previous_next_aux("link_prev", ?T("Previous"), PrevId) ++ [?C(" ")] ++ links_previous_next_aux("link_next", ?T("Next"), NextId))]. links_previous_next_aux(Class, Text, Id) -> case Id of -1 -> []; _ -> [?XAE("a",[{"href",?LINK("show/" ++ integer_to_list(Id))},{"class",Class}], [?C(Text)])] end. %------------------------ get_contacts({LUser, LServer}) -> Fun = fun() -> {selected, _ , Contacts} = run_sql_query("SELECT with_user,with_server,COUNT(*)" " FROM archive_collections" " WHERE us = " ++ get_us_escaped({LUser,LServer}) ++ " AND deleted=0" " GROUP BY with_user,with_server"), Contacts end, run_sql_transaction(LServer, Fun). get_collection_list(Jid, {LUser, LServer}) -> {WithU, WithS, _} = get_jid_escaped(Jid), Fun = fun() -> {selected, _ , List} = run_sql_query("SELECT id,with_user,with_server,with_resource,utc,subject" " FROM archive_collections" " WHERE us = " ++ get_us_escaped({LUser,LServer}) ++ " AND deleted=0 " " AND with_user = " ++ WithU ++ " AND with_server = " ++ WithS), List end, run_sql_transaction(LServer, Fun). get_collection(Id,{LUser,LServer}) -> Fun = fun() -> SUS = get_us_escaped({LUser,LServer}), {selected, _ , [{WithU, WithS, WithR, Utc, Subject}] } = run_sql_query( "SELECT with_user,with_server,with_resource,utc,subject" " FROM archive_collections" " WHERE id = '" ++ ejabberd_odbc:escape(Id) ++ "'" " AND us = " ++ SUS), %If the previous query fail, that mean the collection doesn't exist or is not % one of the users connection. {selected, _ , List} = run_sql_query("SELECT utc,dir,body" " FROM archive_messages" " WHERE coll_id = '" ++ ejabberd_odbc:escape(Id) ++ "'"), NextId = case run_sql_query("SELECT id" " FROM archive_collections" " WHERE us = " ++ SUS ++ " AND deleted=0 " " AND with_user ='" ++ ejabberd_odbc:escape(WithU) ++ "'" ++ " AND with_server ='" ++ ejabberd_odbc:escape(WithS) ++ "'" ++ " AND utc > '" ++ Utc ++ "'" ++ " ORDER BY utc LIMIT 1") of {selected, _ , [{V1}]} -> V1; _ -> -1 end, PrevId = case run_sql_query("SELECT id" " FROM archive_collections" " WHERE us = " ++ SUS ++ " AND deleted=0 " " AND with_user ='" ++ ejabberd_odbc:escape(WithU) ++ "'" ++ " AND with_server ='" ++ ejabberd_odbc:escape(WithS) ++ "'" ++ " AND utc < '" ++ Utc ++ "'" ++ " ORDER BY utc DESC LIMIT 1") of {selected, _ , [{V2}]} -> V2; _ -> -1 end, { {WithU,WithS,WithR} , Utc, Subject , List, {PrevId,NextId}} end, run_sql_transaction(LServer, Fun). change_subject(Id,Subject,{LUser,LServer}) -> run_sql_transaction(LServer, fun() -> run_sql_query( "UPDATE archive_collections" " SET subject='"++ ejabberd_odbc:escape(Subject)++"'," " change_utc=NOW(), change_by='webview'" " WHERE id = '" ++ ejabberd_odbc:escape(Id) ++ "'" " AND us = " ++ get_us_escaped({LUser,LServer})) end). delete_collection(Id,{LUser,LServer}) -> run_sql_transaction(LServer, fun() -> run_sql_query( "UPDATE archive_collections" " SET deleted=1, subject='', thread = '', extra = '', prev_id = NULL, next_id = NULL," " change_utc=NOW(), change_by='webview'" " WHERE id = '" ++ ejabberd_odbc:escape(Id) ++ "'" " AND us = " ++ get_us_escaped({LUser,LServer})) end). %------------------------ % from mod_archive_odbc run_sql_query(Query) -> ?MYDEBUG("running query: ~p", [lists:flatten(Query)]), case catch ejabberd_odbc:sql_query_t(Query) of {'EXIT', Err} -> ?ERROR_MSG("unhandled exception during query: ~p", [Err]), exit(Err); {error, Err} -> ?ERROR_MSG("error during query: ~p", [Err]), throw({error, Err}); aborted -> ?ERROR_MSG("query aborted ~p", [Query]), throw(aborted); R -> ?MYDEBUG("query result: ~p", [R]), R end. run_sql_transaction(LServer, F) -> DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), case ejabberd_odbc:sql_transaction(DBHost, F) of {atomic, R} -> ?MYDEBUG("succeeded transaction: ~p", [R]), R; {error, Err} -> {error, Err}; E -> ?ERROR_MSG("failed transaction: ~p, stack: ~p", [E, process_info(self(),backtrace)]), {error, ?ERR_INTERNAL_SERVER_ERROR} end. get_us_escaped({LUser, LServer}) -> "'" ++ ejabberd_odbc:escape(LUser ++ "@" ++ LServer) ++ "'". get_jid_escaped({LUser, LServer, LResource}) -> {"'" ++ ejabberd_odbc:escape(LUser), "'" ++ ejabberd_odbc:escape(LServer), "'" ++ ejabberd_odbc:escape(LResource)}; get_jid_escaped(#jid{luser = LUser, lserver = LServer, lresource=LResource}) -> {"'" ++ ejabberd_odbc:escape(LUser) ++ "'", "'" ++ ejabberd_odbc:escape(LServer) ++ "'", "'" ++ ejabberd_odbc:escape(LResource) ++ "'"}. decode_integer(Val) when is_integer(Val) -> Val; decode_integer(null) -> -1; decode_integer(Val) -> list_to_integer(Val). escape_str(null) -> ""; escape_str(Str) -> Str.