From c3596c3b847285e882fea1affcf669acee0c0eb3 Mon Sep 17 00:00:00 2001 From: nico Date: Fri, 20 Jul 2018 16:12:53 +0200 Subject: + first release with basic functionality + added systemd service file + added README with instructions --- .gitignore | 0 README.MD | 15 +++- magicbot.service.dummy | 13 +++ main.py | 226 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 4 deletions(-) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 README.MD create mode 100644 magicbot.service.dummy create mode 100644 main.py diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/README.MD b/README.MD old mode 100755 new mode 100644 index 4fc3196..32e5add --- a/README.MD +++ b/README.MD @@ -6,12 +6,11 @@ Do not opperate this bot on foreign servers. ### functions - query xmpp server software and version [XEP-0092](https://xmpp.org/extensions/xep-0092.html) - query xmpp server uptime [XEP-0012](https://xmpp.org/extensions/xep-0012.html) -- displaying a help output +- display help output - respond to username being mentioned ### todo -- [ ] query xmpp server contact addresses [XEP-0157](https://xmpp.org/extensions/xep-0157.html) - - [x] iq is being catched but the answer needs to be extracted out of it +- [ ] extract xmpp server contact addresses [XEP-0157](https://xmpp.org/extensions/xep-0157.html) from result - [ ] Github Webhook ### install @@ -20,6 +19,7 @@ Do not opperate this bot on foreign servers. - configparser - datetime - random +- validators #### configuration `bot.cfg` replace dummy file with correct credentials/ parameters @@ -33,4 +33,11 @@ nick=mucnickname [ADMIN] admins=admins ( ! muc nick and not the jid nickname) ```` - If done correctly `./main.py &` and enjoy your bot. \ No newline at end of file + +##### systemd +Copy the systemd dummy file into systemd service folder. +`systemdctl daemon-reload` and `systemctl start magicbot.service` to start the bot. +If it is neccecary to start the bot automatically when the system boots do `systemctl enable magicbot.service`. + +#### starting the bot without systemd +Got to the bots directory and run `./main.py &`. \ No newline at end of file diff --git a/magicbot.service.dummy b/magicbot.service.dummy new file mode 100644 index 0000000..44ae41f --- /dev/null +++ b/magicbot.service.dummy @@ -0,0 +1,13 @@ + [Unit] + 2 Description=SlixXMPP service bot + 3 After=network.target ejabberd.service + 4 + 5 [Service] + 6 Type=simple + 7 ExecStart=/usr/bin/python3 /path/to/main.py + 8 Restart=on-failure + 9 RestartSec=60s +10 User=nico +11 +12 [Install] +13 WantedBy=multi-user.target diff --git a/main.py b/main.py new file mode 100644 index 0000000..e170d43 --- /dev/null +++ b/main.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" +import asyncio +import configparser +import logging +import slixmpp +import ssl +import validators +from argparse import ArgumentParser +from datetime import datetime, timedelta +from random import randint +from slixmpp.exceptions import XMPPError + + +class QueryBot(slixmpp.ClientXMPP): + """ A simple Slixmpp bot with some features """ + def __init__(self, jid, password, room, nick): + slixmpp.ClientXMPP.__init__(self, jid, password) + + self.room = room + self.nick = nick + + # session start event, starting point for the presence and roster requests + self.add_event_handler('session_start', self.start) + + # register handler to recieve both groupchat and normal message events + self.add_event_handler('message', self.message) + + def start(self, event): + """ + Arguments: + event -- An empty dictionary. The session_start event does not provide any additional data. + """ + self.send_presence() + self.get_roster() + + # If a room password is needed, use: password=the_room_password + self.plugin['xep_0045'].join_muc(self.room, self.nick, wait=True) + + @staticmethod + def precheck(line): + """ + pre check function + - check that keywords are used properly + - check that following a keyword a proper jid is following + :param line: line from message body + :return: true if correct + """ + keywords = ["!help", "!uptime", "!version", "!contact"] + proper_domain, proper_key = False, False + + try: + # check for valid keyword in position 0 + if line[0] in keywords: + proper_key = True + else: + return + + # help command is used + if line[0] == "!help": + proper_domain = True + # check if domain is valid + elif validators.domain(line[1]): + proper_domain = True + else: + return + except IndexError: + pass + + return proper_key and proper_domain + + @asyncio.coroutine + def message(self, msg): + """ + Arguments: + msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see + how it may be used. + """ + + # catch self messages to prevent self flooding + if msg['mucnick'] == self.nick: + return + + if self.nick in msg['body']: + # answer with predefined text when mucnick is used + self.send_message(mto=msg['from'].bare, mbody=notice_answer(msg['mucnick']), mtype=msg['type']) + + for line in msg['body'].splitlines(): + """ split multiline messages into lines to check every line for keywords """ + line = line.split(sep= " ") + + if self.precheck(line): + """ true if keyword and domain are valid """ + # Display help + if line[0] == '!help': + """ display help when keyword !help is recieved """ + self.send_message(mto=msg['from'].bare, mbody=help_doc(), mtype=msg['type']) + + # XEP-0072: Server Version + if line[0] == '!version': + """ query the server software version of the specified domain, defined by XEP-0092 """ + try: + version = yield from self['xep_0092'].get_version(line[1]) + + if msg['type'] == "groupchat": + text = "%s: %s is running %s version %s on %s" % (msg['mucnick'], line[1], version[ + 'software_version']['name'], version['software_version']['version'], version[ + 'software_version']['os']) + else: + text = "%s is running %s version %s on %s" % (line[1], version['software_version'][ + 'name'], version['software_version']['version'], version['software_version']['os']) + + self.send_message(mto=msg['from'].bare, mbody=text, mtype=msg['type']) + except NameError: + pass + except XMPPError: + pass + + # XEP-0012: Last Activity + if line[0] == '!uptime': + """ query the server uptime of the specified domain, defined by XEP-0012 """ + try: + # try if domain[0] is set if not just pass + last_activity = yield from self['xep_0012'].get_last_activity(line[1]) + uptime = datetime(1, 1, 1) + timedelta(seconds=last_activity['last_activity']['seconds']) + + if msg['type'] == "groupchat": + text = "%s: %s is running since %d days %d hours %d minutes" % (msg['mucnick'], line[1], + uptime.day - 1, uptime.hour, + uptime.minute) + else: + text = "%s is running since %d days %d hours %d minutes" % (line[1], uptime.day - 1, + uptime.hour, uptime.minute) + self.send_message(mto=msg['from'].bare, mbody=text, mtype=msg['type']) + except NameError: + pass + except XMPPError: + pass + + # XEP-0157: Contact Addresses for XMPP Services + if line[0] == "!contact": + """ query the XEP-0030: Service Discovery and extract contact information """ + try: + result = yield from self['xep_0030'].get_info(jid=line[1], cached=False) + server_info = [] + for field in result['disco_info']['form']: + var = field['var'] + if field['type'] == 'hidden' and var == 'FORM_TYPE': + title = field['value'][0] + continue + sep = '\n ' + len(var) * ' ' + field_value = field.get_value(convert=False) + value = sep.join(field_value) if isinstance(field_value, list) else field_value + server_info.append('%s: %s' % (var, value)) + + self.send_message(mto=msg['from'].bare, mbody=server_info, mtype=msg['type']) + except NameError: + pass + except XMPPError: + pass + + # TODO + # append all results to single message send just once + else: + pass + + +def help_doc(): + helpfile = {'help': '!help -- display this text', + 'version': '!version domain.tld -- receive XMPP server version', + 'uptime':'!uptime domain.tld -- receive XMPP server uptime', + 'contact': '!contact domain.tld -- receive XMPP server contact address info'} + return "".join(['%s\n' % (value) for (_, value) in helpfile.items()]) + + +def notice_answer(nickname): + possible_answers = {'1': 'I heard that, %s.', + '2': 'I am sorry for that %s.', + '3': '%s did you try turning it off and on again?'} + return possible_answers[str(randint(1, len(possible_answers)))] % nickname + +if __name__ == '__main__': + # command line arguments. + parser = ArgumentParser() + 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) + parser.add_argument('-D', '--dev', help='set logging to console', action='store_const', dest='logfile', + const="", default='bot.log') + args = parser.parse_args() + + # logging + logging.basicConfig(filename=args.logfile, level=args.loglevel, format='%(levelname)-8s %(message)s') + logger = logging.getLogger(__name__) + + # configfile + config = configparser.RawConfigParser() + config.read('./bot.cfg') + args.jid = config.get('Account', 'jid') + args.password = config.get('Account', 'password') + args.room = config.get('MUC', 'rooms') + args.nick = config.get('MUC', 'nick') + args.admins = config.get('ADMIN', 'admins') + + # init the bot and register used slixmpp plugins + xmpp = QueryBot(args.jid, args.password, args.room, args.nick) + xmpp.ssl_version = ssl.PROTOCOL_TLSv1_2 + xmpp.register_plugin('xep_0012') # Last Activity + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0045') # Multi-User Chat + xmpp.register_plugin('xep_0060') # PubSub + xmpp.register_plugin('xep_0085') # Chat State Notifications + xmpp.register_plugin('xep_0092') # Software Version + xmpp.register_plugin('xep_0199') # XMPP Ping + + # connect and start receiving stanzas + xmpp.connect() + xmpp.process() -- cgit v1.2.3-18-g5258