%%%---------------------------------------------------------------------- %%% File : mod_antispam_rtbl.erl %%% Author : Stefan Strigler %%% Purpose : Collection of RTBL specific functionality %%% Created : 20 Mar 2025 by Stefan Strigler %%% %%% %%% ejabberd, Copyright (C) 2025 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(mod_antispam_rtbl). -author('stefan@strigler.de'). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("mod_antispam.hrl"). -define(SERVICE_MODULE, mod_antispam). -define(SERVICE_JID_PREFIX, "rtbl-"). -export([parse_blocked_domains/1, parse_pubsub_event/1, pubsub_event_handler/1, request_blocked_domains/3, subscribe/3, unsubscribe/3]). %% @format-begin subscribe(RTBLHost, RTBLDomainsNode, From) -> FromJID = service_jid(From), SubIQ = #iq{type = set, to = jid:make(RTBLHost), from = FromJID, sub_els = [#pubsub{subscribe = #ps_subscribe{jid = FromJID, node = RTBLDomainsNode}}]}, ?DEBUG("Sending subscription request:~n~p", [xmpp:encode(SubIQ)]), ejabberd_router:route_iq(SubIQ, subscribe_result, self()). -spec unsubscribe(binary() | none, binary(), binary()) -> ok. unsubscribe(none, _PSNode, _From) -> ok; unsubscribe(RTBLHost, RTBLDomainsNode, From) -> FromJID = jid:make(From), SubIQ = #iq{type = set, to = jid:make(RTBLHost), from = FromJID, sub_els = [#pubsub{unsubscribe = #ps_unsubscribe{jid = FromJID, node = RTBLDomainsNode}}]}, ejabberd_router:route_iq(SubIQ, unsubscribe_result, self()). -spec request_blocked_domains(binary() | none, binary(), binary()) -> ok. request_blocked_domains(none, _PSNode, _From) -> ok; request_blocked_domains(RTBLHost, RTBLDomainsNode, From) -> IQ = #iq{type = get, from = jid:make(From), to = jid:make(RTBLHost), sub_els = [#pubsub{items = #ps_items{node = RTBLDomainsNode}}]}, ?DEBUG("Requesting RTBL blocked domains from ~s:~n~p", [RTBLHost, xmpp:encode(IQ)]), ejabberd_router:route_iq(IQ, blocked_domains, self()). -spec parse_blocked_domains(stanza()) -> #{binary() => any()} | undefined. parse_blocked_domains(#iq{to = #jid{lserver = LServer}, type = result} = IQ) -> ?DEBUG("parsing iq-result items: ~p", [IQ]), [#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer), case xmpp:get_subtag(IQ, #pubsub{}) of #pubsub{items = #ps_items{node = RTBLDomainsNode, items = Items}} -> ?DEBUG("Got items:~n~p", [Items]), parse_items(Items); _ -> undefined end. -spec parse_pubsub_event(stanza()) -> #{binary() => any()}. parse_pubsub_event(#message{to = #jid{lserver = LServer}} = Msg) -> [#rtbl_service{node = RTBLDomainsNode}] = mod_antispam:get_rtbl_services_option(LServer), case xmpp:get_subtag(Msg, #ps_event{}) of #ps_event{items = #ps_items{node = RTBLDomainsNode, items = Items, retract = RetractIds}} -> maps:merge(retract_items(RetractIds), parse_items(Items)); Other -> ?WARNING_MSG("Couldn't extract items: ~p", [Other]), #{} end. -spec parse_items([ps_item()]) -> #{binary() => any()}. parse_items(Items) -> lists:foldl(fun(#ps_item{id = ID}, Acc) -> %% TODO extract meta/extra instructions maps:put(ID, true, Acc) end, #{}, Items). -spec retract_items([binary()]) -> #{binary() => false}. retract_items(Ids) -> lists:foldl(fun(ID, Acc) -> Acc#{ID => false} end, #{}, Ids). -spec service_jid(binary()) -> jid(). service_jid(Host) -> jid:make(<<>>, Host, <>). %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- -spec pubsub_event_handler(stanza()) -> drop | stanza(). pubsub_event_handler(#message{from = FromJid, to = #jid{lserver = LServer, lresource = <>}} = Msg) -> ?DEBUG("Got RTBL message:~n~p", [Msg]), From = jid:encode(FromJid), [#rtbl_service{host = RTBLHost}] = mod_antispam:get_rtbl_services_option(LServer), case RTBLHost of From -> ParsedItems = parse_pubsub_event(Msg), Proc = gen_mod:get_module_proc(LServer, ?SERVICE_MODULE), gen_server:cast(Proc, {update_blocked_domains, ParsedItems}), %% FIXME what's the difference between `{drop, ...}` and `{stop, {drop, ...}}`? drop; _Other -> ?INFO_MSG("Got unexpected message from ~s to rtbl resource:~n~p", [From, Msg]), Msg end; pubsub_event_handler(Acc) -> ?DEBUG("unexpected something on pubsub_event_handler: ~p", [Acc]), Acc.