From d305f8adf3fa7c4f154f003bdf16ce42b1895ffd Mon Sep 17 00:00:00 2001 From: nico Date: Sat, 6 Oct 2018 13:16:27 +0200 Subject: small improvements * leading 0 fix * validation function improvements * moved strings.py and misc files to /common/ --- classes/strings.py | 54 ------------------------------------------------------ classes/xep.py | 8 ++++---- 2 files changed, 4 insertions(+), 58 deletions(-) delete mode 100644 classes/strings.py (limited to 'classes') diff --git a/classes/strings.py b/classes/strings.py deleted file mode 100644 index 6866a31..0000000 --- a/classes/strings.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -from random import randint - - -class StaticAnswers: - """ - collection of callable static/ semi-static strings - """ - def __init__(self, nick=""): - self.nickname = nick - self.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', - 'xep': '!xep XEP Number -- recieve information about the specified XEP'} - self.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?'} - self.error_messages = { - '1': 'not reachable', - '2': 'not a valid target' - } - self.keywords = { - "keywords": ["!help", "!uptime", "!version", "!contact", "!xep"], - "domain_keywords": ["!uptime", "!version", "!contact"], - "no_arg_keywords": ["!help"], - "number_keywords": ["!xep"] - } - - def keys(self, arg="", keyword='keywords'): - if arg == 'list': - try: - return self.keywords[keyword] - except KeyError: - return self.keywords['keywords'] - else: - return self.keywords - - def gen_help(self): - helpdoc = "\n".join(['%s' % value for (_, value) in self.helpfile.items()]) - return helpdoc - - def gen_answer(self): - possible_answers = self.possible_answers - return possible_answers[str(randint(1, possible_answers.__len__()))] % self.nickname - - def error(self,code): - try: - text = self.error_messages[str(code)] - except KeyError: - return 'unknown error' - return text diff --git a/classes/xep.py b/classes/xep.py index 9e4f61f..9e8414c 100644 --- a/classes/xep.py +++ b/classes/xep.py @@ -13,7 +13,7 @@ class XEPRequest: self.message_type = msg['type'] self.muc_nick = msg['mucnick'] - self.reqxep = str(xepnumber) + self.reqxep = int(xepnumber) self.xeplist = None self.acceptedxeps = list() @@ -33,14 +33,14 @@ class XEPRequest: etag = head.headers['etag'] if local_etag == etag: - with open("xeplist.xml", "r") as file: + with open("./common/xeplist.xml", "r") as file: self.xeplist = ET.fromstring(file.read()) else: r = s.get("https://xmpp.org/extensions/xeplist.xml") r.encoding = 'utf-8' local_etag = head.headers['etag'] - with open("xeplist.xml", "w") as file: + with open("./common/xeplist.xml", "w") as file: file.write(r.content.decode()) self.xeplist = ET.fromstring(r.content.decode()) @@ -61,7 +61,7 @@ class XEPRequest: result = list() # if requested number is inside acceptedxeps continou - if self.reqxep in self.acceptedxeps: + if str(self.reqxep) in self.acceptedxeps: searchstring = ".//*[@accepted='true']/[number='%s']" % self.reqxep for item in self.xeplist.findall(searchstring): -- cgit v1.2.3-54-g00ecf From 9d452717786908d5a1e72e392d8c20239e415adf Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 10 Oct 2018 17:43:01 +0200 Subject: + added etree implementation to grab contact addresses from bare xml --- classes/servercontact.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 classes/servercontact.py (limited to 'classes') diff --git a/classes/servercontact.py b/classes/servercontact.py new file mode 100644 index 0000000..27b72f0 --- /dev/null +++ b/classes/servercontact.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import defusedxml.ElementTree as Et + + +class ServerContact: + def __init__(self, contact, msg, target): + self.contact = contact + self.message = msg + self.target = target + + self.possible_vars = ['abuse-addresses', + 'admin-addresses', + 'feedback-addresses', + 'sales-addresses', + 'security-addresses', + 'support-addresses'] + + def process(self): + # get etree from base xml + iq = Et.fromstring(str(self.contact)) + + # check if query is a valid result query + if iq.find('{http://jabber.org/protocol/disco#info}query'): + # only init result dict if result query is present + result = dict() + + # extract query from iq + query = iq.find('{http://jabber.org/protocol/disco#info}query') + + # extract jabber:x:data from query + xdata = query.findall('{jabber:x:data}x') + + # check for multiple x nodes + for x in range(len(xdata)): + + # iterate over all x nodes + for child in xdata[x]: + + # if node has a var attribute that matches our list process + if child.attrib['var'] in self.possible_vars: + # add section to result dict and append info + result[child.attrib['var']] = list() + for value in child: + result[child.attrib['var']].append(value.text) + + return result + + def format_contact(self): + result = self.process() + + if result: + text = "contact addresses for %s are\n" % self.target + + for key in result.keys(): + if result[key]: + addr = ' , '.join(result[key]) + text += "- %s : %s\n" % (key, addr) + else: + text = "%s has no contact addresses configured." % self.target + + return text -- cgit v1.2.3-54-g00ecf From 559ab280ca705bca200823a0493308b10aba1dd4 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 11 Oct 2018 19:28:18 +0200 Subject: * new uptime.py file - removed asyncio from import * fixed bug with muc jid parser * outsorced validate and dedup to misc.py --- classes/uptime.py | 45 ++++++++++++++++++++++++++++ main.py | 90 ++++++++++++++----------------------------------------- 2 files changed, 67 insertions(+), 68 deletions(-) create mode 100644 classes/uptime.py (limited to 'classes') diff --git a/classes/uptime.py b/classes/uptime.py new file mode 100644 index 0000000..96c8685 --- /dev/null +++ b/classes/uptime.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + + +# XEP-0012: Last Activity +class LastActivity: + """ query the server uptime of the specified domain, defined by XEP-0012 """ + def __init__(self, session, msg, target): + self.session = session + self.nick = msg['mucnick'] + self.message_type = msg['type'] + self.target = target + + async def query(self): + last_activity = await self.session['xep_0012'].get_last_activity(jid=self.target) + seconds = await last_activity['last_activity']['seconds'] + + return seconds + + async def format_values(self, granularity=4): + seconds = await self.query() + #seconds = last_activity['last_activity']['seconds'] + uptime = [] + intervals = ( + ('years', 31536000), # 60 * 60 * 24 * 365 + ('weeks', 604800), # 60 * 60 * 24 * 7 + ('days', 86400), # 60 * 60 * 24 + ('hours', 3600), # 60 * 60 + ('minutes', 60), + ('seconds', 1) + ) + for name, count in intervals: + value = seconds // count + if value: + seconds -= value * count + if value == 1: + name = name.rstrip('s') + uptime.append("{} {}".format(value, name)) + result = ' '.join(uptime[:granularity]) + + if self.message_type == "groupchat": + text = "%s: %s is running since %s" % (self.nick, self.target, result) + else: + text = "%s is running since %s" % (self.target, result) + + return text diff --git a/main.py b/main.py index ef4345d..c285e2f 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ - Slixmpp: The Slick XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of Slixmpp. + James the MagicXMPP Bot + build with Slick XMPP Library + Copyright (C) 2018 Nico Wellpott See the file LICENSE for copying permission. """ -import asyncio import slixmpp import ssl -import validators import configparser import logging @@ -19,7 +17,9 @@ from slixmpp.exceptions import XMPPError import common.misc as misc from common.strings import StaticAnswers -from classes.functions import Version, LastActivity, ContactInfo, HandleError +from classes.functions import Version, ContactInfo, HandleError +from classes.servercontact import ServerContact +from classes.uptime import LastActivity from classes.xep import XEPRequest @@ -39,7 +39,7 @@ class QueryBot(slixmpp.ClientXMPP): # session start event, starting point for the presence and roster requests self.add_event_handler('session_start', self.start) - # register recieve handler for both groupchat and normal message events + # register receive handler for both groupchat and normal message events self.add_event_handler('message', self.message) def start(self, event): @@ -50,65 +50,19 @@ class QueryBot(slixmpp.ClientXMPP): self.get_roster() # If a room password is needed, use: password=the_room_password - for rooms in self.room.split(sep=","): - self.plugin['xep_0045'].join_muc(rooms, self.nick, wait=True) - - def validate(self, wordlist, index): - """ - validation method to reduce malformed querys and unnecessary connection attempts - :param wordlist: words separated by " " from the message - :param index: keyword index inside the message - :return: true if valid - """ - # keyword inside the message - argument = wordlist[index] - - # check if argument is in the argument list - if argument in StaticAnswers().keys(arg='list'): - # if argument uses a domain check for occurence in list and check domain - if argument in StaticAnswers().keys(arg='list', keyword='domain_keywords'): - try: - target = wordlist[index + 1] - if validators.domain(target): - return True - except IndexError: - # except an IndexError if a keywords is the last word in the message - return False - - # check if number keyword is used if true check if target is assignable - elif argument in StaticAnswers().keys(arg='list', keyword='number_keywords'): - try: - if wordlist[index + 1]: - return True - except IndexError: - # except an IndexError if target is not assignable - return False - # check if argument is inside no_arg list - elif argument in StaticAnswers().keys(arg='list', keyword="no_arg_keywords"): - return True - else: - return False - else: - return False - - def deduplicate(self, reply): - """ - deduplication method for the result list - :param list reply: list containing strings - :return: list containing unique strings - """ - reply_dedup = list() - for item in reply: - if item not in reply_dedup: - reply_dedup.append(item) - - return reply_dedup + if self.room: + for rooms in self.room.split(sep=","): + self.plugin['xep_0045'].join_muc(rooms, self.nick, wait=True) async def message(self, msg): """ :param msg: received message stanza """ - data = self.data + data = { + 'words': list(), + 'reply': list(), + 'queue': list() + } # catch self messages to prevent self flooding if msg['mucnick'] == self.nick: @@ -144,18 +98,18 @@ class QueryBot(slixmpp.ClientXMPP): target = data['words'][index + 1] try: if keyword == '!uptime': - last_activity = await self['xep_0012'].get_last_activity(jid=target) - self.data['reply'].append(LastActivity(last_activity, msg, target).format_values()) + data['reply'].append(LastActivity(self, msg, target).format_values()) + + #last_activity = await self['xep_0012'].get_last_activity(jid=target) + #data['reply'].append(LastActivity(last_activity, msg, target).format_values()) elif keyword == "!version": version = await self['xep_0092'].get_version(jid=target) - self.data['reply'].append(Version(version, msg, target).format_version()) - + data['reply'].append(Version(version, msg, target).format_version()) elif keyword == "!contact": - last_activity = await self['xep_0012'].get_last_activity(jid=target) - self.data['reply'].append(LastActivity(last_activity, msg, target).format_values()) - + contact = await self['xep_0030'].get_info(jid=target, cached=False) + data['reply'].append(ServerContact(contact, msg, target).format_contact()) elif keyword == "!xep": data['reply'].append(XEPRequest(msg, target).format()) -- cgit v1.2.3-54-g00ecf From 0c313565f2b649366f7382dc1b3f28a3e80f4ffc Mon Sep 17 00:00:00 2001 From: nico Date: Tue, 6 Nov 2018 23:43:11 +0100 Subject: simplification and major rework * updated gitignore file * partly reworked servercontact implementation * complete rework of uptime, version * part rework of xep requests + added more comments to xep requests + added opt_arg to version, xep and contact * complete rework of validate function * updated HandleError function * part rework of StaticStrings function + implemented data dictionary to hold all data in main bot + added message_ids * complete rework of queue building and deduplication --- .gitignore | 4 +- classes/functions.py | 108 ------------------------------------------- classes/servercontact.py | 45 ++++++++++++++---- classes/uptime.py | 43 +++++++++-------- classes/version.py | 39 ++++++++++++++++ classes/xep.py | 85 ++++++++++++++++++++++------------ common/misc.py | 76 +++++++++++++++++------------- common/strings.py | 13 +++--- main.py | 118 +++++++++++++++++++++++++++++++---------------- 9 files changed, 284 insertions(+), 247 deletions(-) delete mode 100644 classes/functions.py create mode 100644 classes/version.py (limited to 'classes') diff --git a/.gitignore b/.gitignore index 103d4a6..6391a21 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,6 @@ target/ # .idea .idea -.etag bot\.cfg -xeplist.xml +common/xeplist.xml +common/.etag \ No newline at end of file diff --git a/classes/functions.py b/classes/functions.py deleted file mode 100644 index a8ed356..0000000 --- a/classes/functions.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# XEP-0072: Server Version -class Version: - def __init__(self, version, msg, target): - self.version = version['software_version']['version'] - self.os = version['software_version']['os'] - self.name = version['software_version']['name'] - self.nick = msg['mucnick'] - self.message_type = msg['type'] - self.target = target - - def format_version(self): - if self.message_type == "groupchat": - text = "%s: %s is running %s version %s on %s" % (self.nick, self.target, self.name, self.version, self.os) - else: - text = "%s is running %s version %s on %s" % (self.target, self.name, self.version, self.os) - - return text - - -# XEP-0012: Last Activity -class LastActivity: - """ query the server uptime of the specified domain, defined by XEP-0012 """ - def __init__(self, last_activity, msg, target): - self.last_activity = last_activity - self.nick = msg['mucnick'] - self.message_type = msg['type'] - self.target = target - - def format_values(self, granularity=4): - seconds = self.last_activity['last_activity']['seconds'] - uptime = [] - intervals = ( - ('years', 31536000), # 60 * 60 * 24 * 365 - ('weeks', 604800), # 60 * 60 * 24 * 7 - ('days', 86400), # 60 * 60 * 24 - ('hours', 3600), # 60 * 60 - ('minutes', 60), - ('seconds', 1) - ) - for name, count in intervals: - value = seconds // count - if value: - seconds -= value * count - if value == 1: - name = name.rstrip('s') - uptime.append("{} {}".format(value, name)) - result = ' '.join(uptime[:granularity]) - - if self.message_type == "groupchat": - text = "%s: %s is running since %s" % (self.nick, self.target, result) - else: - text = "%s is running since %s" % (self.target, result) - - return text - - -# XEP-0157: Contact Addresses for XMPP Services -class ContactInfo: - def __init__(self, contact, msg, target): - self.contact = contact - self.message = msg - self.target = target - - def format_contact(self): - server_info = [] - sep = ' , ' - possible_vars = ['abuse-addresses', - 'admin-addresses', - 'feedback-addresses', - 'sales-addresses', - 'security-addresses', - 'support-addresses'] - - for field in self.contact['disco_info']['form']: - var = field['var'] - if var in possible_vars: - 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)) - - if server_info: - text = "contact addresses for %s are" % self.target - for count in range(server_info.__len__()): - text += "\n" + server_info[count] - else: - text = "%s has no contact addresses configured." % self.target - - return text - - -# class handeling XMPPError exeptions -class HandleError: - def __init__(self, error, msg, key, target="target missing"): - self.error = error - self.message = msg - self.key = key - self.target = target - - def build_report(self): - condition = self.error.condition - keyword = self.key[1:] - - text = "There was an error requesting " + self.target + '\'s ' + keyword + " : " + condition - - return text diff --git a/classes/servercontact.py b/classes/servercontact.py index 27b72f0..ea7216d 100644 --- a/classes/servercontact.py +++ b/classes/servercontact.py @@ -2,12 +2,13 @@ import defusedxml.ElementTree as Et +# XEP-0157: Contact Addresses for XMPP Services class ServerContact: - def __init__(self, contact, msg, target): - self.contact = contact - self.message = msg - self.target = target - + """ + plugin to process the server contact addresses from a disco query + """ + def __init__(self): + # init all necessary variables self.possible_vars = ['abuse-addresses', 'admin-addresses', 'feedback-addresses', @@ -15,6 +16,9 @@ class ServerContact: 'security-addresses', 'support-addresses'] + self.contact = None + self.target, self.opt_arg = None, None + def process(self): # get etree from base xml iq = Et.fromstring(str(self.contact)) @@ -36,8 +40,16 @@ class ServerContact: # iterate over all x nodes for child in xdata[x]: + # if one opt_arg is defined return just that one + if self.opt_arg in self.possible_vars: + if child.attrib['var'] == self.opt_arg: + # add section to result dict and append info + result[child.attrib['var']] = list() + for value in child: + result[child.attrib['var']].append(value.text) + # if node has a var attribute that matches our list process - if child.attrib['var'] in self.possible_vars: + elif child.attrib['var'] in self.possible_vars: # add section to result dict and append info result[child.attrib['var']] = list() for value in child: @@ -45,17 +57,30 @@ class ServerContact: return result - def format_contact(self): + def format(self, query, target, opt_arg): + self.contact = query + + self.target = target + self.opt_arg = opt_arg + result = self.process() + # if result is present continue if result: text = "contact addresses for %s are\n" % self.target + # if opt_arg is present and member of possible_vars change text line + if opt_arg in self.possible_vars: + text = "%s for %s are\n" % (self.opt_arg, self.target) + for key in result.keys(): - if result[key]: - addr = ' , '.join(result[key]) - text += "- %s : %s\n" % (key, addr) + addr = ' , '.join(result[key]) + text += "- %s : %s\n" % (key, addr) else: text = "%s has no contact addresses configured." % self.target + # if opt_arg is present and member of possible_vars but the key is empty change text line + if opt_arg in self.possible_vars: + text = "%s for %s are not defined." % (self.opt_arg, self.target) + return text diff --git a/classes/uptime.py b/classes/uptime.py index 96c8685..6eb15dd 100644 --- a/classes/uptime.py +++ b/classes/uptime.py @@ -3,23 +3,19 @@ # XEP-0012: Last Activity class LastActivity: - """ query the server uptime of the specified domain, defined by XEP-0012 """ - def __init__(self, session, msg, target): - self.session = session - self.nick = msg['mucnick'] - self.message_type = msg['type'] - self.target = target - - async def query(self): - last_activity = await self.session['xep_0012'].get_last_activity(jid=self.target) - seconds = await last_activity['last_activity']['seconds'] - - return seconds + """ + query the server uptime of the specified domain, defined by XEP-0012 + """ + def __init__(self): + # init all necessary variables + self.last_activity = None + self.target, self.opt_arg = None, None - async def format_values(self, granularity=4): - seconds = await self.query() - #seconds = last_activity['last_activity']['seconds'] + def process(self, granularity=4): + seconds = self.last_activity['last_activity']['seconds'] uptime = [] + + # touple with displayable time sections intervals = ( ('years', 31536000), # 60 * 60 * 24 * 365 ('weeks', 604800), # 60 * 60 * 24 * 7 @@ -28,6 +24,8 @@ class LastActivity: ('minutes', 60), ('seconds', 1) ) + + # for every element in possible time section process the seconds for name, count in intervals: value = seconds // count if value: @@ -37,9 +35,16 @@ class LastActivity: uptime.append("{} {}".format(value, name)) result = ' '.join(uptime[:granularity]) - if self.message_type == "groupchat": - text = "%s: %s is running since %s" % (self.nick, self.target, result) - else: - text = "%s is running since %s" % (self.target, result) + # insert values into result string + text = "%s is running since %s" % (self.target, result) return text + + def format(self, query, target, opt_arg): + self.last_activity = query + + self.target = target + self.opt_arg = opt_arg + + reply = self.process() + return reply diff --git a/classes/version.py b/classes/version.py new file mode 100644 index 0000000..1e9ef7e --- /dev/null +++ b/classes/version.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + + +# XEP-0072: Server Version +class Version: + """ + process and format a version query + """ + def __init__(self): + # init all necessary variables + self.software_version = None + self.target, self.opt_arg = None, None + + def format_result(self): + # list of all possible opt_arg + possible_opt_args = ["version", "os", "name"] + + name = self.software_version['name'] + version = self.software_version['version'] + os = self.software_version['os'] + + # if opt_arg is given member of possible_opt_args list return that element + if self.opt_arg in possible_opt_args: + text = "%s: %s" % (self.opt_arg, self.software_version[self.opt_arg]) + + # otherwise return full version string + else: + text = "%s is running %s version %s on %s" % (self.target, name, version, os) + + return text + + def format(self, query, target, opt_arg): + self.software_version = query['software_version'] + + self.target = target + self.opt_arg = opt_arg + + reply = self.format_result() + return reply diff --git a/classes/xep.py b/classes/xep.py index 9e8414c..98f4b78 100644 --- a/classes/xep.py +++ b/classes/xep.py @@ -1,19 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os import requests -import defusedxml.ElementTree as ET +import defusedxml.ElementTree as et class XEPRequest: - def __init__(self, msg, xepnumber): - """ - class which requests the header of the referenced xep - :param xepnumber: number int or str to request the xep for - """ - self.message_type = msg['type'] - self.muc_nick = msg['mucnick'] + """ + class which requests the header of the referenced xep + """ + def __init__(self): + # init all necessary variables + self.reqxep, self.opt_arg = None, None - self.reqxep = int(xepnumber) self.xeplist = None self.acceptedxeps = list() @@ -21,20 +20,30 @@ class XEPRequest: """ query and save the current xep list to reduce network bandwidth """ - try: - with open(".etag") as file: + # check if etag header is present if not set local_etag to "" + if os.path.isfile("./common/.etag"): + with open("./common/.etag") as file: local_etag = file.read() - except FileNotFoundError: + else: local_etag = "" with requests.Session() as s: + # head request the xeplist.xml s.headers.update({'Accept': 'application/xml'}) head = s.head("https://xmpp.org/extensions/xeplist.xml") etag = head.headers['etag'] + # compare etag with local_etag if they match up no request is made if local_etag == etag: with open("./common/xeplist.xml", "r") as file: - self.xeplist = ET.fromstring(file.read()) + self.xeplist = et.fromstring(file.read()) + + # if the connection is not possible use cached xml if present + elif os.path.isfile("./common/xeplist.xml") and head.status_code != 200: + with open("./common/xeplist.xml", "r") as file: + self.xeplist = et.fromstring(file.read()) + + # in any other case request the latest xml else: r = s.get("https://xmpp.org/extensions/xeplist.xml") r.encoding = 'utf-8' @@ -42,9 +51,9 @@ class XEPRequest: with open("./common/xeplist.xml", "w") as file: file.write(r.content.decode()) - self.xeplist = ET.fromstring(r.content.decode()) + self.xeplist = et.fromstring(r.content.decode()) - with open('.etag', 'w') as string: + with open('./common/.etag', 'w') as string: string.write(local_etag) # populate xep comparison list @@ -54,34 +63,52 @@ class XEPRequest: def get(self): """ function to query the xep entry if xepnumber is present in xeplist - :return: nicely formatted xep header information + :return: formatted xep header information """ + # all possible subtags grouped by location + last_revision_tags = ["date", "version", "initials", "remark"] + xep_tags = ["number", "title", "abstract", "type", "status", "approver", "shortname", "sig", "lastcall"] + # check if xeplist is accurate self.req_xeplist() result = list() - # if requested number is inside acceptedxeps continou + # if requested number is member of acceptedxeps continue if str(self.reqxep) in self.acceptedxeps: searchstring = ".//*[@accepted='true']/[number='%s']" % self.reqxep for item in self.xeplist.findall(searchstring): - for x in range(1,5): - result.append(item[x].tag + " : " + item[x].text) - + # if the opt_arg references is member of xeptag return only that tag + if self.opt_arg in xep_tags: + query = item.find(self.opt_arg) + result.append("%s : %s" % (query.tag, query.text)) + + # if the opt_arg references is member of last-revision_tags return only that tag + elif self.opt_arg in last_revision_tags: + query = item.find("last-revision").find(self.opt_arg) + result.append("%s : %s" % (query.tag, query.text)) + + # in any other case return the general answer + else: + result_opts = ["title", "type", "abstract", "status"] + for tag in result_opts: + result.append(item.find(tag).text) + + # if the requested number is no member of acceptedxeps and/or not accepted return error. else: - if self.message_type == "groupchat": - result.append(self.muc_nick + " : " + "XEP-" + str(self.reqxep) + " : is not available.") - else: - result.append("XEP-" + str(self.reqxep) + " : is not available.") + result.append("XEP-%s : is not available." % self.reqxep) return result - def format(self): + def format(self, query, target, opt_arg): + """ + :param target: number int or str to request the xep for + :return: + """ + self.reqxep = int(target) + self.opt_arg = opt_arg + reply = self.get() - if self.message_type == "groupchat": - text = "%s: " % self.muc_nick - reply[0] = text + reply[0] text = '\n'.join(reply) - return text diff --git a/common/misc.py b/common/misc.py index 3961b8d..bbf9c55 100755 --- a/common/misc.py +++ b/common/misc.py @@ -17,43 +17,53 @@ def deduplicate(reply): return reply_dedup -def validate(wordlist, index): +def validate(keyword, target): """ validation method to reduce malformed querys and unnecessary connection attempts - :param wordlist: words separated by " " from the message - :param index: keyword index inside the message + :param keyword: used keyword + :param target: provided target :return: true if valid """ - # keyword inside the message - argument = wordlist[index] - - # check if argument is in the argument list - if argument in StaticAnswers().keys(arg='list'): - # if argument uses a domain check for occurrence in list and check domain - if argument in StaticAnswers().keys(arg='list', keyword='domain_keywords'): - try: - target = wordlist[index + 1] - if validators.domain(target): - return True - elif validators.email(target): - return True - - except IndexError: - # except an IndexError if a keywords is the last word in the message - return False - - # check if number keyword is used if true check if target is assignable - elif argument in StaticAnswers().keys(arg='list', keyword='number_keywords'): - try: - if wordlist[index + 1]: - return True - except IndexError: - # except an IndexError if target is not assignable - return False - # check if argument is inside no_arg list - elif argument in StaticAnswers().keys(arg='list', keyword="no_arg_keywords"): + # check if keyword is in the argument list + if keyword in StaticAnswers().keys(): + + # if keyword in domain_keywords list + if keyword in StaticAnswers().keys('domain_keywords'): + # if target is a domain / email return True + if validators.domain(target): + return True + elif validators.email(target): + return True + + # check if keyword is in number_keyword list + elif keyword in StaticAnswers().keys('number_keywords'): + # if target only consists of digits return True + if target.isdigit(): + return True + + # if keyword is in no_arg_keywords list return True + elif keyword in StaticAnswers().keys("no_arg_keywords"): return True - else: - return False + + # if the target could not be validated until this return False else: return False + + +# +class HandleError: + """ + simple XMPP error / exception class formating the error condition + """ + def __init__(self, error, key, target): + # init all necessary variables + self.error = error + self.key = key + self.target = target + + def report(self): + # return the formatted result string to the user + condition = self.error.condition + text = "There was an error requesting %s's %s : %s" % (self.target, self.key, condition) + + return text diff --git a/common/strings.py b/common/strings.py index 6866a31..faac65c 100644 --- a/common/strings.py +++ b/common/strings.py @@ -29,14 +29,13 @@ class StaticAnswers: "number_keywords": ["!xep"] } - def keys(self, arg="", keyword='keywords'): - if arg == 'list': - try: - return self.keywords[keyword] - except KeyError: - return self.keywords['keywords'] + def keys(self, key=""): + # if specific keyword in referenced return that + if key in self.keywords.keys(): + return self.keywords[key] + # in any other case return the whole dict else: - return self.keywords + return self.keywords["keywords"] def gen_help(self): helpdoc = "\n".join(['%s' % value for (_, value) in self.helpfile.items()]) diff --git a/main.py b/main.py index c285e2f..42d5349 100644 --- a/main.py +++ b/main.py @@ -17,8 +17,8 @@ from slixmpp.exceptions import XMPPError import common.misc as misc from common.strings import StaticAnswers -from classes.functions import Version, ContactInfo, HandleError from classes.servercontact import ServerContact +from classes.version import Version from classes.uptime import LastActivity from classes.xep import XEPRequest @@ -29,6 +29,14 @@ class QueryBot(slixmpp.ClientXMPP): self.ssl_version = ssl.PROTOCOL_TLSv1_2 self.room = room self.nick = nick + self.use_message_ids = True + + self.functions = { + "!uptime": LastActivity(), + "!contact": ServerContact(), + "!version": Version(), + "!xep": XEPRequest() + } self.data = { 'words': list(), @@ -72,56 +80,88 @@ class QueryBot(slixmpp.ClientXMPP): # add pre predefined text to reply list data['reply'].append(StaticAnswers(msg['mucnick']).gen_answer()) - # building the queue - # double splitting to exclude whitespaces - data['words'] = " ".join(msg['body'].split()).split(sep=" ") - - # check all words in side the message for possible hits - for x in enumerate(data['words']): - # check word for match in keywords list - for y in StaticAnswers().keys(arg='list'): - # if so queue the keyword and the position in the string - if x[1] == y: - # only add job to queue if domain is valid - if misc.validate(data['words'], x[0]): - data['queue'].append({str(y): x[0]}) + data = self.build_queue(data, msg) # queue for job in data['queue']: - for keyword in job: - index = job[keyword] + keys = list(job.keys()) + keyword = keys[0] - if keyword == '!help': - data['reply'].append(StaticAnswers().gen_help()) - continue + target = job[keyword][0] + opt_arg = job[keyword][1] + query = None - target = data['words'][index + 1] - try: - if keyword == '!uptime': - data['reply'].append(LastActivity(self, msg, target).format_values()) + if keyword == '!help': + data['reply'].append(StaticAnswers().gen_help()) + continue - #last_activity = await self['xep_0012'].get_last_activity(jid=target) - #data['reply'].append(LastActivity(last_activity, msg, target).format_values()) + try: + if keyword == "!uptime": + query = await self['xep_0012'].get_last_activity(jid=target) - elif keyword == "!version": - version = await self['xep_0092'].get_version(jid=target) - data['reply'].append(Version(version, msg, target).format_version()) + elif keyword == "!version": + query = await self['xep_0092'].get_version(jid=target) - elif keyword == "!contact": - contact = await self['xep_0030'].get_info(jid=target, cached=False) - data['reply'].append(ServerContact(contact, msg, target).format_contact()) + elif keyword == "!contact": + query = await self['xep_0030'].get_info(jid=target, cached=False) - elif keyword == "!xep": - data['reply'].append(XEPRequest(msg, target).format()) + except XMPPError as error: + logging.INFO(misc.HandleError(error, keyword, target).report()) + data['reply'].append(misc.HandleError(error, keyword, target).report()) + continue - except XMPPError as error: - data['reply'].append(HandleError(error, msg, keyword, target).build_report()) + data["reply"].append(self.functions[keyword].format(query=query, target=target, opt_arg=opt_arg)) # remove None type from list and send all elements if list(filter(None.__ne__, data['reply'])) and data['reply']: - reply = misc.deduplicate(data['reply']) + + # if msg type is groupchat prepend mucnick + if msg["type"] == "groupchat": + data["reply"][0] = "%s: " % msg["mucnick"] + data["reply"][0] + + # reply = misc.deduplicate(data['reply']) + reply = data["reply"] self.send_message(mto=msg['from'].bare, mbody="\n".join(reply), mtype=msg['type']) + def build_queue(self, data, msg): + # building the queue + # double splitting to exclude whitespaces + data['words'] = " ".join(msg['body'].split()).split(sep=" ") + wordcount = len(data["words"]) + + # check all words in side the message for possible hits + for x in enumerate(data['words']): + # check for valid keywords + index = x[0] + keyword = x[1] + + # match all words starting with ! and member of no_arg_keywords + if keyword.startswith("!") and keyword in StaticAnswers().keys("no_arg_keywords"): + data['queue'].append({keyword: [None, None]}) + + # matching all words starting with ! and member of keywords + elif keyword.startswith("!") and keyword in StaticAnswers().keys("keywords"): + # init variables to circumvent IndexErrors + target, opt_arg = None, None + + # compare to wordcount if assignment is possible + if index + 1 < wordcount: + target = data["words"][index + 1] + + if index + 2 < wordcount: + if not data["words"][index + 2].startswith("!"): + opt_arg = data["words"][index + 2] + + # only add job to queue if domain is valid + if misc.validate(keyword, target): + logging.debug("Item added to queue %s" % {str(keyword): [target, opt_arg]}) + data['queue'].append({str(keyword): [target, opt_arg]}) + + # deduplicate queue elements + data["queue"] = misc.deduplicate(data["queue"]) + + return data + if __name__ == '__main__': # command line arguments. @@ -130,12 +170,12 @@ if __name__ == '__main__': 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') + 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') + logging.basicConfig(filename=args.logfile, level=logging.INFO, format='%(levelname)-8s %(message)s') logger = logging.getLogger(__name__) # configfile -- cgit v1.2.3-54-g00ecf From d7fc664d3be4634a693a24fa98ff3f15d2c97c41 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 7 Nov 2018 00:37:24 +0100 Subject: * corrected CamelCase * corrected logging.INFO to .info * small changes to HandleError class --- classes/xep.py | 9 ++++----- common/misc.py | 6 +++--- main.py | 3 ++- 3 files changed, 9 insertions(+), 9 deletions(-) (limited to 'classes') diff --git a/classes/xep.py b/classes/xep.py index 98f4b78..a74c30f 100644 --- a/classes/xep.py +++ b/classes/xep.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import requests -import defusedxml.ElementTree as et +import defusedxml.ElementTree as Et class XEPRequest: @@ -36,12 +35,12 @@ class XEPRequest: # compare etag with local_etag if they match up no request is made if local_etag == etag: with open("./common/xeplist.xml", "r") as file: - self.xeplist = et.fromstring(file.read()) + self.xeplist = Et.fromstring(file.read()) # if the connection is not possible use cached xml if present elif os.path.isfile("./common/xeplist.xml") and head.status_code != 200: with open("./common/xeplist.xml", "r") as file: - self.xeplist = et.fromstring(file.read()) + self.xeplist = Et.fromstring(file.read()) # in any other case request the latest xml else: @@ -51,7 +50,7 @@ class XEPRequest: with open("./common/xeplist.xml", "w") as file: file.write(r.content.decode()) - self.xeplist = et.fromstring(r.content.decode()) + self.xeplist = Et.fromstring(r.content.decode()) with open('./common/.etag', 'w') as string: string.write(local_etag) diff --git a/common/misc.py b/common/misc.py index bbf9c55..1350a2a 100755 --- a/common/misc.py +++ b/common/misc.py @@ -57,13 +57,13 @@ class HandleError: """ def __init__(self, error, key, target): # init all necessary variables - self.error = error + self.text = error.text + self.condition = error.condition self.key = key self.target = target def report(self): # return the formatted result string to the user - condition = self.error.condition - text = "There was an error requesting %s's %s : %s" % (self.target, self.key, condition) + text = "%s, %s resulted in: %s" % (self.text, self.key, self.condition) return text diff --git a/main.py b/main.py index 42d5349..5f8f220 100644 --- a/main.py +++ b/main.py @@ -60,6 +60,7 @@ class QueryBot(slixmpp.ClientXMPP): # If a room password is needed, use: password=the_room_password if self.room: for rooms in self.room.split(sep=","): + logging.debug("joining: %s" % rooms) self.plugin['xep_0045'].join_muc(rooms, self.nick, wait=True) async def message(self, msg): @@ -106,7 +107,7 @@ class QueryBot(slixmpp.ClientXMPP): query = await self['xep_0030'].get_info(jid=target, cached=False) except XMPPError as error: - logging.INFO(misc.HandleError(error, keyword, target).report()) + logging.info(misc.HandleError(error, keyword, target).report()) data['reply'].append(misc.HandleError(error, keyword, target).report()) continue -- cgit v1.2.3-54-g00ecf From de56a9315cef894a2f3c3a9b39cad4ed10f55491 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 7 Nov 2018 01:14:45 +0100 Subject: small error correction * +x to main.py + added catch for a None response as some xeps have addition tags some do not have --- classes/xep.py | 9 +++++++-- main.py | 0 2 files changed, 7 insertions(+), 2 deletions(-) mode change 100644 => 100755 main.py (limited to 'classes') diff --git a/classes/xep.py b/classes/xep.py index a74c30f..fdddb22 100644 --- a/classes/xep.py +++ b/classes/xep.py @@ -75,17 +75,16 @@ class XEPRequest: # if requested number is member of acceptedxeps continue if str(self.reqxep) in self.acceptedxeps: searchstring = ".//*[@accepted='true']/[number='%s']" % self.reqxep + query = None for item in self.xeplist.findall(searchstring): # if the opt_arg references is member of xeptag return only that tag if self.opt_arg in xep_tags: query = item.find(self.opt_arg) - result.append("%s : %s" % (query.tag, query.text)) # if the opt_arg references is member of last-revision_tags return only that tag elif self.opt_arg in last_revision_tags: query = item.find("last-revision").find(self.opt_arg) - result.append("%s : %s" % (query.tag, query.text)) # in any other case return the general answer else: @@ -93,6 +92,12 @@ class XEPRequest: for tag in result_opts: result.append(item.find(tag).text) + # append opt_arg results to the result list + if query is not None: + result.append("%s : %s" % (query.tag, query.text)) + else: + result.append("%s does not have a %s tag." % (self.reqxep, self.opt_arg)) + # if the requested number is no member of acceptedxeps and/or not accepted return error. else: result.append("XEP-%s : is not available." % self.reqxep) diff --git a/main.py b/main.py old mode 100644 new mode 100755 -- cgit v1.2.3-54-g00ecf From d1b8090ee8f773628cc5eb04971b575debfec249 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 7 Nov 2018 14:47:55 +0100 Subject: NoneType Bug fix * fixed NoneType tag bug --- classes/xep.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'classes') diff --git a/classes/xep.py b/classes/xep.py index fdddb22..f5fae61 100644 --- a/classes/xep.py +++ b/classes/xep.py @@ -66,7 +66,7 @@ class XEPRequest: """ # all possible subtags grouped by location last_revision_tags = ["date", "version", "initials", "remark"] - xep_tags = ["number", "title", "abstract", "type", "status", "approver", "shortname", "sig", "lastcall"] + xep_tags = ["number", "title", "abstract", "type", "status", "approver", "shortname", "sig", "lastcall", "date", "version", "initials", "remark"] # check if xeplist is accurate self.req_xeplist() @@ -75,16 +75,22 @@ class XEPRequest: # if requested number is member of acceptedxeps continue if str(self.reqxep) in self.acceptedxeps: searchstring = ".//*[@accepted='true']/[number='%s']" % self.reqxep - query = None for item in self.xeplist.findall(searchstring): # if the opt_arg references is member of xeptag return only that tag if self.opt_arg in xep_tags: - query = item.find(self.opt_arg) - # if the opt_arg references is member of last-revision_tags return only that tag - elif self.opt_arg in last_revision_tags: - query = item.find("last-revision").find(self.opt_arg) + # if the opt_arg references is member of last_revision_tags return only that subtag + if self.opt_arg in last_revision_tags: + query = item.find("last-revision").find(self.opt_arg) + else: + query = item.find(self.opt_arg) + + # append opt_arg results to the result list + if query is not None: + result.append("%s : %s" % (query.tag, query.text)) + else: + result.append("%s does not have a %s tag." % (self.reqxep, self.opt_arg)) # in any other case return the general answer else: @@ -92,12 +98,6 @@ class XEPRequest: for tag in result_opts: result.append(item.find(tag).text) - # append opt_arg results to the result list - if query is not None: - result.append("%s : %s" % (query.tag, query.text)) - else: - result.append("%s does not have a %s tag." % (self.reqxep, self.opt_arg)) - # if the requested number is no member of acceptedxeps and/or not accepted return error. else: result.append("XEP-%s : is not available." % self.reqxep) -- cgit v1.2.3-54-g00ecf From cd1442e216abf564daceaef5fe45555587eef69b Mon Sep 17 00:00:00 2001 From: nico Date: Fri, 9 Nov 2018 19:42:21 +0100 Subject: code quality improvements - remove unused variable * better iteration of xdata nodes - removed unnecessary else --- classes/servercontact.py | 8 ++++---- common/misc.py | 3 +-- common/strings.py | 4 ++-- main.py | 6 ------ 4 files changed, 7 insertions(+), 14 deletions(-) (limited to 'classes') diff --git a/classes/servercontact.py b/classes/servercontact.py index ea7216d..749a2c3 100644 --- a/classes/servercontact.py +++ b/classes/servercontact.py @@ -34,11 +34,11 @@ class ServerContact: # extract jabber:x:data from query xdata = query.findall('{jabber:x:data}x') - # check for multiple x nodes - for x in range(len(xdata)): + # iterate over all nodes with the xdata tag + for node in xdata: - # iterate over all x nodes - for child in xdata[x]: + # iterate over all child elements in node + for child in node: # if one opt_arg is defined return just that one if self.opt_arg in self.possible_vars: diff --git a/common/misc.py b/common/misc.py index e0df882..abcc05e 100755 --- a/common/misc.py +++ b/common/misc.py @@ -46,8 +46,7 @@ def validate(keyword, target): return True # if the target could not be validated until this return False - else: - return False + return False # diff --git a/common/strings.py b/common/strings.py index faac65c..7a10471 100644 --- a/common/strings.py +++ b/common/strings.py @@ -33,9 +33,9 @@ class StaticAnswers: # if specific keyword in referenced return that if key in self.keywords.keys(): return self.keywords[key] + # in any other case return the whole dict - else: - return self.keywords["keywords"] + return self.keywords["keywords"] def gen_help(self): helpdoc = "\n".join(['%s' % value for (_, value) in self.helpfile.items()]) diff --git a/main.py b/main.py index 97a1110..24171cd 100755 --- a/main.py +++ b/main.py @@ -38,12 +38,6 @@ class QueryBot(slixmpp.ClientXMPP): "!xep": XEPRequest() } - self.data = { - 'words': list(), - 'reply': list(), - 'queue': list() - } - # session start event, starting point for the presence and roster requests self.add_event_handler('session_start', self.start) -- cgit v1.2.3-54-g00ecf From b6b84108ed24939a78ed4f6240b830860967136f Mon Sep 17 00:00:00 2001 From: nico Date: Fri, 9 Nov 2018 20:52:31 +0100 Subject: quality of life improvement to servercontact + added opt_arg abbreviation --- classes/servercontact.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'classes') diff --git a/classes/servercontact.py b/classes/servercontact.py index 749a2c3..031ef67 100644 --- a/classes/servercontact.py +++ b/classes/servercontact.py @@ -19,7 +19,27 @@ class ServerContact: self.contact = None self.target, self.opt_arg = None, None + def opt_arg_abbreviation(self): + """ + optional argument abbreviation function + if the provided string > 2 characters the most likely key will be chosen + :return: completes the opt_arg to the most likely one + """ + # if opt_argument is smaller then 2 pass to prohibit multiple answers + if len(self.opt_arg) < 2: + pass + + abbr = str(self.opt_arg) + possible_abbr = ["abuse-addresses", "admin-addresses", "feedback-addresses", "sales-addresses", + "security-addresses", "support-addresses"] + + # searches the best match in the list of possible_abbr and completes the opt_arg to that + self.opt_arg = [s for s in possible_abbr if s.startswith(abbr)][0] + def process(self): + # optional argument abbreviation + self.opt_arg_abbreviation() + # get etree from base xml iq = Et.fromstring(str(self.contact)) @@ -70,7 +90,7 @@ class ServerContact: text = "contact addresses for %s are\n" % self.target # if opt_arg is present and member of possible_vars change text line - if opt_arg in self.possible_vars: + if self.opt_arg in self.possible_vars: text = "%s for %s are\n" % (self.opt_arg, self.target) for key in result.keys(): @@ -80,7 +100,7 @@ class ServerContact: text = "%s has no contact addresses configured." % self.target # if opt_arg is present and member of possible_vars but the key is empty change text line - if opt_arg in self.possible_vars: + if self.opt_arg in self.possible_vars: text = "%s for %s are not defined." % (self.opt_arg, self.target) return text -- cgit v1.2.3-54-g00ecf