#!/usr/bin/env swipl
/* echobot.pl
Author: Gimenez, Christian.
Copyright (C) 2026 Gimenez, Christian
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 3 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, see .
2026-05-03
*/
:- module(echobot, []).
/** echobot: Echo bot example.
To run this script, set BOTJID and BOTPASS with the JID and password to connect
to the server.
@author Christian Gimenez
@license GPLv3
*/
:- license(gplv3).
:- use_module(library(xmpp/xmpp)).
:- use_module(library(xmpp/xmpp_core)).
:- use_module(library(xpath)).
:- initialization(main, main).
opt_type(d, debug, boolean).
opt_help(debug, 'Enable debug mode').
% ----------
% Handlers
%! after_connect_handler(+Args: list)
%
% Handler for after_connect hook.
%
% This handler sends the chatty presence of the bot.
%
% @param Args Ignored (no processing to this data).
after_connect_handler(_Args) :-
send_presence('Echo Bot ready!', [show('chat')]),
debug(echobot, 'Echobot connected!', []).
%! received_presence_handler(+Args: list)
%
% Handler for received_presence hook.
%
% Executed when a new presence update stanza is received. This handler only
% prints to standard output the update presence.
%
% # Example
% Presences stanza are XML strings such as follows:
%
% ```
%
%
% PDih23uetra123aeoua//1uerta1234taro45096tor=
%
%
%
%
% b123ebaeba1234be384ab39952138e9281a123f3
%
%
% Not in my phone
% away
%
% ```
%
% The `Args` argument receives a list of the XML string parsed using
% load_xml/3. Also, the list contains the current client status. For instance,
% the XML string presented before is parsed as the following list:
%
% ```
% [
% data([element(presence,
% ['xml:lang'=en,
% to='alice@example.net/echobot',
% from='bob@example.net'],
% [element(c,
% [xmlns='urn:xmpp:caps'],
% [element(hash,
% [xmlns='urn:xmpp:hashes:2',algo='sha-256'],
% ['PDih23uetra123aeoua//1uerta1234taro45096tor='])
% ]),
% element(c,
% [xmlns='http://jabber.org/protocol/caps',
% hash='sha-1',
% ver='29g1I++jyf7QyZgmeQdOWtbHTZ0=',
% node='https://cheogram.com'],
% []),
% element(idle,[xmlns='urn:xmpp:idle:1',since='2026-05-03T14:51:48.684Z'],[]),
% element(x,[xmlns='vcard-temp:x:update'],
% [element(photo,[],[b123ebaeba1234be384ab39952138e9281a123f3])
% ]),
% element(delay,
% [from='bob@example.net',
% stamp='2026-05-03T14:51:49.130928Z',
% xmlns='urn:xmpp:delay'],
% []),
% element(status,[],['Not in my phone']),
% element(show,[],[away])
% ])
% ]),
% current_state(connected)
% ]
% ```
received_presence_handler(Args) :-
debug(echobot,'Handling presence: ~q', [Args]),
member(data(XML), Args),
(xpath(XML, //presence/status(text), Text), !; Text = '[No status]'),
(xpath(XML, //presence/show(text), Show), !; Show = 'Online'),
xpath(XML, //presence(@to), To),
xpath(XML, //presence(@from), From), !,
format('Presence from ~q to ~q: (~q) ~q', [From, To, Show, Text]),
nl, flush.
received_presence_handler(Args) :- !,
debug(echobot, 'Failed to handle presence: ~q', [Args]).
%! received_message_handler(+Args: list)
%
% Handler for the `received_message` hook.
%
% This handlers is executed when the bot receives a message stanza. It extracts
% the from JID and the text message, and sends it back.
%
% # Argument example
% The `Args` argument is an XML parsed into a Prolog list. The message stanza
% is parsed using load_xml/3, and passed to this hook handler.
%
% The following is an XML string example received by the client and not yet
% processed by the xmpp_core library.
%
% ```
%
%
%
%
% Hola
%
% ```
%
% This string is parsed and generates a list of terms `DATA`. Then it is added
% to a list as `data(DATA)` with the current XMPP client state. This list is
% unified with Args when calling this handler.
%
% ```
% [
% data([element(message,
% ['xml:lang'=en,
% to='bob@example.net',
% from='alice@example.net/myclient-12345',
% type=chat,
% id='d30b3d1b-67f3-4f4a-a0e0-c3fdc9d36b93'],
% [element(archived,
% [by='bob@example.net',
% id='1777820434735876',
% xmlns='urn:xmpp:mam:tmp'],
% []),
% element('stanza-id',
% [by='bob@example.net',
% id='1777820434735876',
% xmlns='urn:xmpp:sid:0'],
% []),
% element(request,[xmlns='urn:xmpp:receipts'],[]),
% element(markable,[xmlns='urn:xmpp:chat-markers:0'],[]),
% element(body,[],['Hola'])
% ])
% ]),
% current_state(connected)
% ]
% ```
%
% @param Args a list with `[data(XMLParsed), current_state(ClientState)]`. The
% order of data and currest_state is not garanteed.
received_message_handler(Args) :-
debug(echobot, 'Received: ~q', [Args]),
member(data(XML), Args),
xpath(XML, //message/body(text), Text),
xpath(XML, //message(@from), From),
format(atom(NewText), 'Echo: ~q', [Text]),
debug(echobot, 'Sending ~q...', [NewText]),
send_message(From, NewText),
format('Echo to ~q: ~q', [From, NewText]), nl, flush.
received_message_handler(Args) :- !,
debug(echobot, 'Message handler failed with ~q', [Args]).
% -----
enable_debug(Options) :-
option(debug(true), Options, false),
debug(xmpp), debug(xmpp_auth), debug(xmpp_ping),
debug(echobot).
get_envdata(JID, Password) :-
getenv('BOTJID', JID),
getenv('BOTPASS', Password).
get_envdata(_, _) :-
throw(error(no_envvar_set,
context(get_envdata/2, 'Please, set BOTJID and BOTPASS environment variables'))).
main(ArgV) :-
argv_options(ArgV, _Pos, Options),
ignore(enable_debug(Options)),
get_envdata(JID, Password),
add_hook(after_connect, echobot:after_connect_handler),
add_hook(received_message, echobot:received_message_handler),
add_hook(received_presence, echobot:received_presence_handler),
connect(JID, Password),
write('Press ENTER to quit.'), nl,
get_char(_),
disconnect.