#!/usr/bin/env python3

# Secinfo-xmpp - based on muc.py from Slixmpp examples which are
#   Copyright (C) 2010  Nathanael C. Fritz
#
# See the file LICENSE for copying permission.
#  (MIT license - found at https://slixmpp.readthedocs.io/en/latest/license.html)
#
#   Copyright (c) 2025 John Lines - MIT/Expat

import logging
from getpass import getpass
from argparse import ArgumentParser
from typing import Union
import configparser
import os
import sys
import socket

import asyncio
import slixmpp
import subprocess
import time

version = "0.1.11"
announcePrescence = 1
hasFail2ban = False
fail2banJails = []
moderatorNicks = []
# Fail2ban policies - these should end up being per jail
fail2banPolicyLocalBan = "send"
fail2banPolicyLocalUnban = "silent"
fail2banPolicyRemoteBan = "ban-new"
fail2banPolicyRemoteUnban = "noaction"

# If true then send ",s=<nick>" with messages.
#
policySendSource = False

# default to not being a coordinator
isCoordinator = False

# default to not serving information on a web port (only applies to
#  coordinatorss in any case
webPort = 0

configSocket = "/run/secinfo-xmpp/secinfo.sock"

startTime = int(round(time.time()))
localBansSent = 0
localUnbansSent = 0
remoteBansReceived = 0
remoteBansActioned = 0
remoteUnbansReceived = 0
remoteUnbansActioned = 0


class SecBot(slixmpp.ClientXMPP):

    """
    This should be run as a daemon on systems sharing security information
    via a common XMPP group.
    It listens to a socket or  local tcp port and forwards that
    information to the group, for example from fail2ban.
    It also listens to the group for security information from other
    systems, and can, for example, preemptively ban an attacher which
    has attacked another system in the group
    """

    def __init__(self, jid, password, room, nick):
        slixmpp.ClientXMPP.__init__(self, jid, password)

        self.room = room
        self.nick = nick

        # The session_start event will be triggered when
        # the bot establishes its connection with the server
        # and the XML streams are ready for use. We want to
        # listen for this event so that we we can initialize
        # our roster.
        self.add_event_handler("session_start", self.start)

        # The groupchat_message event is triggered whenever a message
        # stanza is received from any chat room. If you also also
        # register a handler for the 'message' event, MUC messages
        # will be processed by both handlers.
        self.add_event_handler("groupchat_message", self.muc_message)

        # The groupchat_presence event is triggered whenever a
        # presence stanza is received from any chat room, including
        # any presences you send yourself. To limit event handling
        # to a single room, use the events muc::room@server::presence,
        # muc::room@server::got_online, or muc::room@server::got_offline.
        self.add_event_handler("muc::%s::got_online" % self.room,
                               self.muc_online)

        # Try to deal with session being closed
        self.add_event_handler("disconnected", self.disconnected)


    async def start(self, event):
        """
        Process the session_start event.

        Typical actions for the session_start event are
        requesting the roster and broadcasting an initial
        presence stanza.

        Arguments:
            event -- An empty dictionary. The session_start
                     event does not provide any additional
                     data.
        """
        await self.get_roster()
        self.send_presence()
        self.plugin['xep_0045'].join_muc(self.room,
                                         self.nick,
                                         # If a room password is needed, use:
                                         # password=the_room_password,
                                         )

    def muc_message(self, msg):
        """
        Process incoming message stanzas from any chat room. Be aware
        that if you also have any handlers for the 'message' event,
        message stanzas may be processed by both handlers, so check
        the 'type' attribute when using a 'message' event handler.

        Whenever the bot's nickname is mentioned, respond to
        the message.

        IMPORTANT: Always check that a message is not from yourself,
                   otherwise you will create an infinite loop responding
                   to your own messages.

        This handler will reply to messages that mention
        the bot's nickname.

        Arguments:
            msg -- The received message stanza. See the documentation
                   for stanza objects and the Message stanza to see
                   how it may be used.
        """
        logging.debug('secinfo-xmpp - xmpp read %s from %s, nick %s',
                              msg['body'],
                              msg['from'],msg['mucnick'])
        if msg['mucnick'] != self.nick and self.nick in msg['body']:
            handlebodyToMe(msg['body'],msg['from'])
        else:
            handlebody(msg['body'],msg['from'])


    def sendmessage(self, messg):
        """
        take a message string and send it to the group chat
        """
        self.send_message(mto=self.room,
            mbody=messg,
            mtype='groupchat')

    def muc_online(self, presence):
        """
        Process a presence stanza from a chat room. In this case,
        presences from users that have just come online are
        handled by sending a welcome message that includes
        the user's nickname and role in the room.

        Arguments:
            presence -- The received presence stanza. See the
                        documentation for the Presence stanza
                        to see how else it may be used.
        """
        global version
        global announcePrescence
        global moderatorNicks
#        print("muc_online called")
#        if presence['muc']['nick'] != self.nick and announcePresence:
        if presence['muc']['nick'] != self.nick:
#            print("about to send message")
            if presence['muc']['role'] == 'moderator':
                thisModerator = presence['muc']['nick']
                if thisModerator not in moderatorNicks:
                    moderatorNicks.append(thisModerator)
                    self.send_message(mto=presence['from'].bare,
                              mbody="Hello, new %s %s secinfo-xmpp version %s"  % (presence['muc']['role'],
                                                      presence['muc']['nick'],version),
                              mtype='groupchat')
                else:
                    self.send_message(mto=presence['from'].bare,
                              mbody="Hello, %s %s secinfo-xmpp version %s"  % (presence['muc']['role'],
                                                      presence['muc']['nick'], version),
                              mtype='groupchat')

# want handle disconnected event - this does not work at present,
# producing a messages
#   INFO     connection_lost: (None,)
#   ERROR    '_asyncio.Future' object is not callable
#  Traceback (most recent call last):
#   File "/usr/lib/python3/dist-packages/slixmpp/xmlstream/xmlstream.py", line 1127, in event
#     handler_callback(data)
#    ~~~~~~~~~~~~~~~~^^^^^^
#TypeError: '_asyncio.Future' object is not callable
#
    def disconnected( self, reason: Union[str, Exception]):
        print(" disconnected called")


# Handler for local socket connections

async def handle_local_secinfo(reader, writer):
    global fail2banPolicyLocalBan
    global fail2banPolicyLocalUnban
    global version
    global localBansSent
    global localUnbansSent
    global remoteBansReceived
    global remoteBansActioned
    global remoteUnbansReceived
    global remoteUnbansActioned
    global startTime
    global policySendSource

    sendThisMessage = True
    data = await reader.read(100)
    rawMessage = data.decode()
    addr = writer.get_extra_info('peername')

    message=rawMessage.strip()
    logging.debug("secinfo-xmpp - socket - Received %s from %s",message,addr)
# Is is a socket command
    if 'xStatus' in message:
        response=bytes('OK','UTF-8')
        writer.write(response)
#    logging.debug(f"Send: {message!r}")
#    writer.write(data)
    elif 'xVersion' in message:
        response=bytes('Version: '+version,'UTF-8')
        writer.write(response)
    elif 'xStats' in message:
        runtime = str(int(round(time.time())) - startTime)
        response = bytes('Running: '+ runtime +
            ' localBansSent: ' + str(localBansSent) +
            ' localUnbansSent: ' + str(localUnbansSent) +
            ' remoteBansReceived: ' + str(remoteBansReceived) +
            ' remoteBansActioned: ' + str(remoteBansActioned) +
            ' remoteUnbansReceived: ' + str(remoteUnbansReceived) +
            ' remoteUnbansActioned: ' + str(remoteUnbansActioned), 'UTF-8')
        writer.write(response)
    else:
        # check if this is a fail2ban unban and policy is silent
        if "t=f2b-u" in message and fail2banPolicyLocalUnban == "silent":
            sendThisMessage = False
        #
        elif "t=f2b-b" in message and fail2banPolicyLocalBan == "warnonly":
            sendThisMessage = False
            logging.warn("secinfo-xmpp - local warnonly %s",message)
        if sendThisMessage:
            if "t=f2b-b" in message:
                localBansSent += 1
            elif "t=f2b-u" in message:
                localUnbansSent += 1
            if policySendSource:
                # applend ,s= and the local nick
                message += ",s="
                message += args.nick
            xmpp.sendmessage(message)
    await writer.drain()

    logging.debug("secinfo-xmpp - socket -Close the connection")
    writer.close()
    await writer.wait_closed()

async def socket_server():
    global configSocket
    # can listen on a Unix socket or a TCP socket.
    if args.port != 0:
        logging.debug('secinfo-xmpp - listening on TCP port %s',args.port)
        server = await asyncio.start_server(
            handle_local_secinfo, '127.0.0.1', args.port)
        addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets)
        logging.debug('xecinfo-xmpp - socket - Serving on %s',addrs)
    elif configSocket != "":
        logging.debug('secinfo-xmpp - listening on socket %s',configSocket)
        # assume for now config socket is actually /run/secinfo-xmpp/secinfo.sock
        if not os.path.isdir('/run/secinfo-xmpp'):
            os.mkdir('/run/secinfo-xmpp')
        server = await asyncio.start_unix_server(
            handle_local_secinfo, configSocket)
        logging.debug('secinfo-xmpp - unixsocket - listening on %s',configSocket)
    else:
        logging.error('secinfo-xmpp - socket_server not TCP or Unix')
        return

    async with server:
        await server.serve_forever()

async def main():
    # Schedule xmpp and port *concurrently*:
    if isCoordinator and webPort != 0:
        await asyncio.gather(
            coord.startWebServer(webPort),
            socket_server(),
            xmpp.connect(),
            )
    else:
        await asyncio.gather(
            socket_server(),
            xmpp.connect(),
        )

def handlecommand(command,sender,toMe):
    global moderatorNicks
    global fail2banPolicyRemoteBan
    global fail2banPolicyRemoteUnban
    global fail2banPolicyLocalBan
    global fail2banPolicyLocalUnban
    # Check this is from an administrator/moderator
    logging.debug('secinfo-xmpp handlecommand %s from %s specific %r', command, sender.jid, toMe)
    splitsender = sender.jid.split("/")
    sendernick = splitsender[1]
    if sendernick not in moderatorNicks:
        logging.error('secinfo-xmpp command %s from non-admin %s',
                        command,sendernick)
        return
    else:
        if command == "exit":
            if ToMe:
                xmpp.sendmessage("OK - exiting")
                sys.exit()
            else:
                xmpp.sendmessage('ignoring general exit command')
                return
        elif command == "version":
            xmpp.sendmessage("Hello version is "+version)
        elif command == "policies":
            xmpp.sendmessage("Hello RemoteBan="+fail2banPolicyRemoteBan+
                ",RemoteUnban="+fail2banPolicyRemoteUnban+
                ",LocalBan="+fail2banPolicyLocalBan+
                ",LocalUnban="+fail2banPolicyLocalUnban)
        elif command == "debug":
            logging.debug('secinfo-xmpp set loglevel to DEBUG')
            logger=logging.getLogger(__name__)
            logger.setLevel(logging.DEBUG)
        elif command == "nodebug":
            logging.debug('secinfo-xmpp set loglevel to INFO')
            logger=logging.getLogger(__name__)
            logger.setLevel(logging.INFO)


def handleF2bBanUnban(f2b_ban_message,sender,ban):
    """
    Handle a Ban or Unban message
    """
    global hasFail2ban
    global fail2banJails
    global fail2banPolicyRemoteBan
    global fail2banPolicyRemoteUnban
    global remoteBansReceived
    global remoteBansActioned
    global remoteUnbansReceived
    global remoteUnbansActioned
    global policySendSource
    global isCoordinator

    logging.debug('secinf0-xmpp f2b ban/unban message %s from %s ban is %r hasFail2ban is %r',f2b_ban_message,sender,ban, hasFail2ban)
    # If coordinator then handle the message, even if fail2ban is not installed on this system
    if isCoordinator:
        coord.handleF2bBanUnban(f2b_ban_message,sender,ban)
    if not hasFail2ban:
        logging.debug("secinfo-xmpp - fail2ban not installed on this system")
        return
    splitsender = sender.jid.split("/")
    sendernick = splitsender[1]
    if sendernick == args.nick:
        logging.debug("secinfo-xmpp - handleF2bBanUban ignore own report")
        return
    if not ban and fail2banPolicyRemoteUnban == 'silent':
        # we do not need to do anything - but still count as an Unban received
        remoteUnbansReceived += 1
        return
    # The jail is the part after j= - If we don't have that jail then return
    fb_dict = dict(element.split("=") for element in f2b_ban_message.split(","))
    thisJail = ''
    if  'j' in fb_dict:
        thisJail = fb_dict['j']
    else:
        logging.error("secinfo-xmpp handleF2bBanUnban cant find jail in $s", f2b_bin_message)
        return
    if thisJail not in fail2banJails:
        # This is not an error, but don't do anything if this jail  is not on this host
        return
    # unpick the source if supplied.
    thisSource = ''
    if 's' in fb_dict:
        thisSource = fb_dict['s']
    # Shortcut much of the processing, dont care if IP was banned locally, but don't log where
    #  there would have been no action anyway as it is for a jail we don't have.
    if ban and fail2banPolicyRemoteBan == 'warnonly':
        logging.warning('secinfo-xmpp - fail2ban message %s received from %s',f2b_ban_message,sender)
        remoteBansReceived += 1
        return
    # See if this IP has been banned already on this host
    thisIp = fb_dict['i']
    ip_is_banned =  subprocess.getoutput("/usr/bin/fail2ban-client get "+thisJail+" banned "+thisIp )
    already_banned = False
    if ip_is_banned == "1":
        already_banned = True
    #
    if already_banned and  ban and fail2banPolicyRemoteBan == 'ban-new':
        remoteBansReceived += 1
        logging.debug("secinfo-xmpp handleF2bBanUnban - not rebanning %s in jail %s",thisIp,thisJail)
        return
    elif already_banned and  ban and fail2banPolicyRemoteBan == 'ban-increment':
        logging.debug('secinfo-xmpp handleF2BanUban - increase ban on %s in jail %s', thisIp,thisJail)
        res=subprocess.getouput("/usr/bin/fail2ban-client set "+thisJail+" attempt "+thisIp)
        return
    elif not already_banned and ban:
        logging.debug("secinfo-xmpp handleF2bBanUnban - banning %s in jail %s",thisIp,thisJail)
        remoteBansReceived += 1
        remoteBansActioned += 1
        res = subprocess.getoutput("/usr/bin/fail2ban-client set "+thisJail+" banip "+thisIp)
        logging.debug("secinfo-xmpp handleF2bBanUnban ban result %s",res)
        return
    # not dealing with Unban just yet

def handlePolicyChange(polmsg, sender):
    """
    Handle a Policy Change message
    """
    global fail2banPolicyRemoteBan
    global fail2banPolicyRemoteUnban
    global fail2banPolicyLocalBan
    global fail2banPolicyLocalUnban
    global policySendSource
    logging.debug("secinfo-xmpp handlePolicyChange %s from %s", polmsg, sender)
    global moderatorNicks
    # Check this is from an administrator/moderator
    splitsender = sender.jid.split("/")
    sendernick = splitsender[1]
    if sendernick not in moderatorNicks:
        logging.error('secinfo-xmpp PolicyChange %s from non-admin %s',
                        polmsg,sendernick)
        return
    polval = polmsg.split('=')
    logging.debug('secinfo-xmpp policy is %s, value is %s', polval[0], polval[1])
    if polval[0] == 'remoteban':
        if polval[1] == 'ban-new':
            logging.warning('secinfo-xmpp changeing policy RemoteBan from %s to %s by order of %s',
                fail2banPolicyRemoteBan, polval[1], sendernick)
            fail2banPolicyRemoteBan = polval[1]
            return
        elif polval[1] == 'ban-increment':
            logging.warning('secinfo-xmpp changeing policy RemoteBan from %s to %s by order of %s',
                fail2banPolicyRemoteBan, polval[1], sendernick)
            fail2banPolicyRemoteBan = polval[1]
            return
        elif polval[1] == 'warnonly':
            logging.warning('secinfo-xmpp changeing policy RemoteBan from %s to %s by order of %s',
                fail2banPolicyRemoteBan, polval[1], sendernick)
            fail2banPolicyRemoteBan = polval[1]
            return
        else:
            logging.warning('secinfo-xmpp NOT changing policy RemoteBan from %s to %s by order of %s',
                            fail2banPolicyRemoteBan, polval[1], sendernick)
            return
    elif polval[0] == 'remoteunban':
        if polval[1] == 'unban':
            logging.warning('secinfo-xmpp changeing policy RemoteUnBan from %s to %s by order of %s',
                fail2banPolicyRemoteUnban, polval[1], sendernick)
            fail2banPolicyRemoteBan = polval[1]
            return
        elif polval[1] == 'noaction':
            logging.warning('secinfo-xmpp changeing policy RemoteUnBan from %s to %s by order of %s',
                fail2banPolicyRemoteUnban, polval[1], sendernick)
            fail2banPolicyRemoteBan = polval[1]
            return
        else:
            logging.warning('secinfo-xmpp NOT changing policy RemoteUnBan from %s to %s by order of %s',
                            fail2banPolicyRemoteUnban, polval[1], sendernick)
            return
    elif polval[0] == 'localban':
        if polval[1] == 'send':
            logging.warning('secinfo-xmpp changeing policy LocalBan from %s to %s by order of %s',
                fail2banPolicyLocalBan, polval[1], sendernick)
            fail2banPolicyLocalBan = polval[1]
            return
        elif polval[1] == 'warnonly':
            logging.warning('secinfo-xmpp changeing policy LocalBan from %s to %s by order of %s',
                fail2banPolicyLocalBan, polval[1], sendernick)
            fail2banPolicyLocalBan = polval[1]
            return
        else:
            logging.warning('secinfo-xmpp NOT changing policy LocalBan from %s to %s by order of %s',
                            fail2banPolicyLocalBan, polval[1], sendernick)
            return
    elif polval[0] == 'localunban':
        if polval[1] == 'send':
            logging.warning('secinfo-xmpp changeing policy LocalUnBan from %s to %s by order of %s',
                fail2banPolicyLocalUnban, polval[1], sendernick)
            fail2banPolicyLocalUnbanBan = polval[1]
            return
        elif polval[1] == 'silent':
            logging.warning('secinfo-xmpp changeing policy LocalUnBan from %s to %s by order of %s',
                fail2banPolicyLocalUnban, polval[1], sendernick)
            fail2banPolicyLocalUnban = polval[1]
            return
        else:
            logging.warning('secinfo-xmpp NOT changing policy LocalUnBan from %s to %s by order of %s',
                            fail2banPolicyLocalUnbanBan, polval[1], sendernick)
            return
    elif polval[0] == 'sendsource':
        if polval[1] == 'false':
            logging.warning('secinfo-xmpp changing policy SendSource from from %s to %s by order of %s',
                           policySendSource, polval[1], sendernick)
            policySendSource = False
        elif polval[1] == 'true':
            logging.warning('secinfo-xmpp changing policy SendSource from from %s to %s by order of %s',
                           policySendSource, polval[1], sendernick)
            policySendSource = True
    else:
        logger.warning('secinfo-xmpp unknow policy %s by order of %s',
                            polval[0], sendernick)


def handlebody(message, sender):
    """
    Handle a message picked up in the XMPP Room

        Arguments:
            message -- The body of the message. This will

            sender -- The nick of the sender
    """
    logging.debug('secinfo-xmpp handlebody "%s" from %s',message,sender)
    if "v=1," in message and message.index("v=1,") == 0:
        # here we have a protocol version 1 message
        v1message = message[4:]
        if "t=cmd,cmd=" in v1message and v1message.index("t=cmd,cmd=") == 0:
            v1cmd = v1message[10:]
            handlecommand(v1cmd,sender,False)
        elif "t=f2b-b," in v1message and v1message.index("t=f2b-b,") == 0:
            f2b_ban_message = v1message[8:]
            handleF2bBanUnban(f2b_ban_message,sender,True)
        elif "t=f2b-u," in v1message and v1message.index("t=f2b-u,") == 0:
            f2b_ban_message = v1message[8:]
            handleF2bBanUnban(f2b_ban_message,sender,False)
        else:
            logging.warning('secinfo-xmpp unknown v1message "%s from %s "',v1message,sender)
    elif "Hello" in message:
        pass
        # don't do anything - ignore these silently
    else:
        logging.warning("secinfo-xmpp unknown message %s from %s",message, sender)

def handlebodyToMe(mymessage, sender):
    """
    Handle message intened for this host, thus a command rather than
    information
    """
    logging.debug('secinfo-xmpp handlebodyToMe "%s" from %s',mymessage,sender)
    # strip off nick
    message=mymessage.split(':')[1].lstrip()
    logging.debug('secinfo-xmpp handlebodyToMe message is "%s" from %s',message,sender)
    if "v=1," in message and message.index("v=1,") == 0:
        # here we have a protocol version 1 message
        v1message = message[4:]
        if "t=cmd,cmd=" in v1message and v1message.index("t=cmd,cmd=") == 0:
            v1cmd = v1message[10:]
            handlecommand(v1cmd,sender,True)
        elif "t=f2b-p" in v1message and v1message.index("t=f2b-p") == 0:
            v1policymsg = v1message[8:]
            handlePolicyChange(v1policymsg,sender)

def discoverActionTargets():
    """
    discover systems, such as fail2ban, and postfix (not yet implemented)
    which are installed on this host, and can be invoked as a result of
    of seeing a message on the XMPP group
    """
    global hasFail2ban
    global fail2banJails
    if os.path.isfile('/usr/bin/fail2ban-client'):
        # could be improved as a check
        hasFail2ban = True
        fail2banJails = subprocess.getoutput("/usr/bin/fail2ban-client status | grep 'Jail' | cut -d: -f2 | tr -d '\t '").split(',')
        logging.debug("secinfo-xmpp - discoverActionTargets - have fail2ban with jails %s",','.join(fail2banJails))

def parseConfig():
    global fail2banPolicyRemoteBan
    global fail2banPolicyRemoteUnban
    global fail2banPolicyLocalBan
    global fail2banPolicyLocalUnban
    global policySendSource
    global isCoordinator
    global webPort
    global dbFileName

    config = configparser.ConfigParser()
    # will look for a config file in argument passed by -f, then
    # .config/secinfo-xmpp/config.ini and then
    #  /etc/secinfo-xmpp/config.ini
    #
    # with open("/home/john/.config/secinfo-xmpp/config.ini","r") as configfile:
    configfile = os.path.expanduser("~/.config/secinfo-xmpp/config.ini")
    if os.path.isfile(configfile):
        config.read(configfile)
    else:
        configfile = "/etc/secinfo-xmpp/config.ini"
        if os.path.isfile(configfile):
            config.read(configfile)
        else:
            sys.exit("Configuration file not found")
 #   print(config.sections())
    args.jid = config['Id']['jid']
    args.password = config['Id']['password']
    args.room = config['Room']['room']
    args.nick = config['Room']['nick']
    args.port = config['Server'].getint('port')

    if args.jid is None:
        args.jid = input("Username: ")
    if args.password is None:
        args.password = getpass("Password: ")
    if args.room is None:
        args.room = input("MUC room: ")
    if args.nick is None:
        args.nick = input("MUC nickname: ")

    # set up policies
    if 'Policies' in config.sections():
        confpolicies = config['Policies']
        fail2banPolicyLocalBan = confpolicies.get('localban','send')
        fail2banPolicyLocalUnban = confpolicies.get('localunban','silent')
        fail2banPolicyRemoteBan = confpolicies.get('remoteban','ban-new')
        fail2banPolicyRemoteUnban = confpolicies.get('remoteunban','noaction')
        policySendSource = confpolicies.getboolean('sendsource',False)
    if 'Coordinator' in config.sections():
        confcoordinator = config['Coordinator']
        isCoordinator = confcoordinator.getboolean('iscoordinator',False)
        webPort = confcoordinator.getint('WebPort',0)
        dbFileName = confcoordinator.get('DbFileName', '/var/local/lib/secinfo-xmpp/secinfo.db')

if __name__ == '__main__':
    # Setup the command line arguments.
    parser = ArgumentParser()

    # Output verbosity options.
    parser.add_argument("-q", "--quiet", help="set logging to ERROR",
                        action="store_const", dest="loglevel",
                        const=logging.ERROR, default=logging.INFO)
    parser.add_argument("-d", "--debug", help="set logging to DEBUG",
                        action="store_const", dest="loglevel",
                        const=logging.DEBUG, default=logging.INFO)

    # JID and password options.
    parser.add_argument("-j", "--jid", dest="jid",
                        help="JID to use")
    parser.add_argument("-p", "--password", dest="password",
                        help="password to use")
    parser.add_argument("-r", "--room", dest="room",
                        help="MUC room to join")
    parser.add_argument("-n", "--nick", dest="nick",
                        help="MUC nickname")
                        # note using -s (socket) as -p used for passwd
    parser.add_argument("-s", "--port", dest="port",
                        help="TCP port")

    args = parser.parse_args()

    # Setup logging.
    logging.basicConfig(level=args.loglevel,
                        format='%(levelname)-8s %(message)s')

    parseConfig()

    if isCoordinator:
        from secinfocoordinator import SecinfoCoordinator
        coord = SecinfoCoordinator(webPort, dbFileName)

    # Discover action tagets
    discoverActionTargets()

    # Setup the SecBot and register plugins. Note that while plugins may
    # have interdependencies, the order in which you register them does
    # not matter.
    xmpp = SecBot(args.jid, args.password, args.room, args.nick)
    xmpp.register_plugin('xep_0030') # Service Discovery
    xmpp.register_plugin('xep_0045') # Multi-User Chat
    xmpp.register_plugin('xep_0199') # XMPP Ping

    # Connect to the XMPP server and start processing XMPP stanzas.
#    xmpp.connect()
    asyncio.run(main())
    asyncio.get_event_loop().run_forever()
