[wip] move files in order to have a clean directory structure

This commit is contained in:
nemunaire 2015-01-03 18:49:01 +01:00
commit 41f7dc2456
31 changed files with 0 additions and 0 deletions

35
nemubot/tools/__init__.py Normal file
View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2014 nemunaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import imp
def reload():
import tools.countdown
imp.reload(tools.countdown)
import tools.date
imp.reload(tools.date)
import tools.web
imp.reload(tools.web)
import tools.xmlparser
imp.reload(tools.xmlparser)
import tools.xmlparser.node
imp.reload(tools.xmlparser.node)

132
nemubot/tools/config.py Normal file
View file

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
from tools.xmlparser import parse_file
logger = logging.getLogger("nemubot.tools.config")
def get_boolean(d, k, default=False):
return ((k in d and d[k].lower() != "false" and d[k].lower() != "off") or
(k not in d and default))
def _load_server(config, xmlnode):
"""Load a server configuration
Arguments:
config -- the global configuration
xmlnode -- the current server configuration node
"""
opts = {
"host": xmlnode["host"],
"ssl": xmlnode.hasAttribute("ssl") and xmlnode["ssl"].lower() == "true",
"nick": xmlnode["nick"] if xmlnode.hasAttribute("nick") else config["nick"],
"owner": xmlnode["owner"] if xmlnode.hasAttribute("owner") else config["owner"],
}
# Optional keyword arguments
for optional_opt in [ "port", "username", "realname",
"password", "encoding", "caps" ]:
if xmlnode.hasAttribute(optional_opt):
opts[optional_opt] = xmlnode[optional_opt]
elif optional_opt in config:
opts[optional_opt] = config[optional_opt]
# Command to send on connection
if "on_connect" in xmlnode:
def on_connect():
yield xmlnode["on_connect"]
opts["on_connect"] = on_connect
# Channels to autojoin on connection
if xmlnode.hasNode("channel"):
opts["channels"] = list()
for chn in xmlnode.getNodes("channel"):
opts["channels"].append((chn["name"], chn["password"])
if chn["password"] is not None
else chn["name"])
# Server/client capabilities
if "caps" in xmlnode or "caps" in config:
capsl = (xmlnode["caps"] if xmlnode.hasAttribute("caps")
else config["caps"]).lower()
if capsl == "no" or capsl == "off" or capsl == "false":
opts["caps"] = None
else:
opts["caps"] = capsl.split(',')
else:
opts["caps"] = list()
# Bind the protocol asked to the corresponding implementation
if "protocol" not in xmlnode or xmlnode["protocol"] == "irc":
from server.IRC import IRC as IRCServer
srvcls = IRCServer
else:
raise Exception("Unhandled protocol '%s'" %
xmlnode["protocol"])
# Initialize the server
return srvcls(**opts)
def load_file(filename, context):
"""Load the configuration file
Arguments:
filename -- the path to the file to load
"""
if os.path.isfile(filename):
config = parse_file(filename)
# This is a true nemubot configuration file, load it!
if config.getName() == "nemubotconfig":
# Preset each server in this file
for server in config.getNodes("server"):
srv = _load_server(config, server)
# Add the server in the context
if context.add_server(srv, get_boolean(server, "autoconnect")):
print("Server '%s' successfully added." % srv.id)
else:
print("Can't add server '%s'." % srv.id)
# Load module and their configuration
for mod in config.getNodes("module"):
context.modules_configuration[mod["name"]] = mod
if get_boolean(mod, "autoload", default=True):
__import__(mod["name"])
# Load files asked by the configuration file
for load in config.getNodes("include"):
load_file(load["path"], context)
# Other formats
else:
print (" Can't load `%s'; this is not a valid nemubot "
"configuration file." % filename)
# Unexisting file, assume a name was passed, import the module!
else:
context.import_module(filename)

108
nemubot/tools/countdown.py Normal file
View file

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2014 nemunaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timezone
import time
def countdown(delta, resolution=5):
sec = delta.seconds
hours, remainder = divmod(sec, 3600)
minutes, seconds = divmod(remainder, 60)
an = int(delta.days / 365.25)
days = delta.days % 365.25
sentence = ""
force = False
if resolution > 0 and (force or an > 0):
force = True
sentence += " %i an" % an
if an > 1:
sentence += "s"
if resolution > 2:
sentence += ","
elif resolution > 1:
sentence += " et"
if resolution > 1 and (force or days > 0):
force = True
sentence += " %i jour" % days
if days > 1:
sentence += "s"
if resolution > 3:
sentence += ","
elif resolution > 2:
sentence += " et"
if resolution > 2 and (force or hours > 0):
force = True
sentence += " %i heure" % hours
if hours > 1:
sentence += "s"
if resolution > 4:
sentence += ","
elif resolution > 3:
sentence += " et"
if resolution > 3 and (force or minutes > 0):
force = True
sentence += " %i minute" % minutes
if minutes > 1:
sentence += "s"
if resolution > 4:
sentence += " et"
if resolution > 4 and (force or seconds > 0):
force = True
sentence += " %i seconde" % seconds
if seconds > 1:
sentence += "s"
return sentence[1:]
def countdown_format(date, msg_before, msg_after, tz=None):
"""Replace in a text %s by a sentence incidated the remaining time
before/after an event"""
if tz is not None:
oldtz = os.environ['TZ']
os.environ['TZ'] = tz
time.tzset()
# Calculate time before the date
try:
if datetime.now(timezone.utc) > date:
sentence_c = msg_after
delta = datetime.now(timezone.utc) - date
else:
sentence_c = msg_before
delta = date - datetime.now(timezone.utc)
except TypeError:
if datetime.now() > date:
sentence_c = msg_after
delta = datetime.now() - date
else:
sentence_c = msg_before
delta = date - datetime.now()
if tz is not None:
os.environ['TZ'] = oldtz
return sentence_c % countdown(delta)

90
nemubot/tools/date.py Normal file
View file

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2014 nemunaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Extraction/Format text
from datetime import datetime, date
import re
xtrdt = re.compile(r'''^.*? (?P<day>[0-9]{1,4}) .+?
(?P<month>[0-9]{1,2}|janvier|january|fevrier|février|february|mars|march|avril|april|mai|maï|may|juin|juni|juillet|july|jully|august|aout|août|septembre|september|october|octobre|oktober|novembre|november|decembre|décembre|december)
(?:.+?(?P<year>[0-9]{1,4}))? (?:[^0-9]+
(?:(?P<hour>[0-9]{1,2})[^0-9]*[h':]
(?:[^0-9]*(?P<minute>[0-9]{1,2})
(?:[^0-9]*[m\":][^0-9]*(?P<second>[0-9]{1,2}))?)?)?.*?)?
$''', re.X)
def extractDate(msg):
"""Parse a message to extract a time and date"""
result = xtrdt.match(msg.lower())
if result is not None:
day = result.group("day")
month = result.group("month")
if month == "janvier" or month == "january" or month == "januar":
month = 1
elif month == "fevrier" or month == "février" or month == "february":
month = 2
elif month == "mars" or month == "march":
month = 3
elif month == "avril" or month == "april":
month = 4
elif month == "mai" or month == "may" or month == "maï":
month = 5
elif month == "juin" or month == "juni" or month == "junni":
month = 6
elif month == "juillet" or month == "jully" or month == "july":
month = 7
elif month == "aout" or month == "août" or month == "august":
month = 8
elif month == "september" or month == "septembre":
month = 9
elif month == "october" or month == "october" or month == "oktober":
month = 10
elif month == "november" or month == "novembre":
month = 11
elif month == "december" or month == "decembre" or month == "décembre":
month = 12
year = result.group("year")
if len(day) == 4:
day, year = year, day
hour = result.group("hour")
minute = result.group("minute")
second = result.group("second")
if year is None:
year = date.today().year
if hour is None:
hour = 0
if minute is None:
minute = 0
if second is None:
second = 1
else:
second = int(second) + 1
if second > 59:
minute = int(minute) + 1
second = 0
return datetime(int(year), int(month), int(day),
int(hour), int(minute), int(second))
else:
return None

38
nemubot/tools/human.py Normal file
View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
def size(size, unit=True):
"""Convert a given byte size to an more human readable way
Argument:
size -- the size to convert
unit -- append the unit at the end of the string
"""
if size <= 0:
return "0 B" if unit else "0"
units = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']
p = math.floor(math.log(size, 2) / 10)
if unit:
return "%.3f %s" % (size / math.pow(1024,p), units[int(p)])
else:
return "%.3f" % (size / math.pow(1024,p))

175
nemubot/tools/web.py Normal file
View file

@ -0,0 +1,175 @@
# coding=utf-8
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from html.entities import name2codepoint
import http.client
import json
import re
import socket
from urllib.parse import quote
from urllib.parse import urlparse
from urllib.request import urlopen
from bot import __version__
from exception import IRCException
from tools.xmlparser import parse_string
def isURL(url):
"""Return True if the URL can be parsed"""
o = urlparse(url)
return o.scheme == "" and o.netloc == "" and o.path == ""
def getScheme(url):
"""Return the protocol of a given URL"""
o = urlparse(url)
return o.scheme
def getHost(url):
"""Return the domain of a given URL"""
return urlparse(url).hostname
def getPort(url):
"""Return the port of a given URL"""
return urlparse(url).port
def getPath(url):
"""Return the page request of a given URL"""
return urlparse(url).path
def getUser(url):
"""Return the page request of a given URL"""
return urlparse(url).username
def getPassword(url):
"""Return the page request of a given URL"""
return urlparse(url).password
# Get real pages
def getURLContent(url, timeout=15):
"""Return page content corresponding to URL or None if any error occurs"""
o = urlparse(url)
if o.netloc == "":
o = urlparse("http://" + url)
if o.scheme == "http":
conn = http.client.HTTPConnection(o.hostname, port=o.port,
timeout=timeout)
elif o.scheme == "https":
conn = http.client.HTTPSConnection(o.hostname, port=o.port,
timeout=timeout)
elif o.scheme is None or o.scheme == "":
conn = http.client.HTTPConnection(o.hostname, port=80, timeout=timeout)
else:
return None
try:
if o.query != '':
conn.request("GET", o.path + "?" + o.query,
None, {"User-agent": "Nemubot v%s" % __version__})
else:
conn.request("GET", o.path, None, {"User-agent":
"Nemubot v%s" % __version__})
except socket.timeout:
return None
except OSError: # [Errno 113] No route to host
return None
except socket.gaierror:
print ("<tools.web> Unable to receive page %s on %s from %s."
% (o.path, o.netloc, url))
return None
try:
res = conn.getresponse()
size = int(res.getheader("Content-Length", 524288))
cntype = res.getheader("Content-Type")
if size > 524288 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"):
return None
data = res.read(size)
# Decode content
charset = "utf-8"
if cntype is not None:
lcharset = res.getheader("Content-Type").split(";")
if len(lcharset) > 1:
for c in charset:
ch = c.split("=")
if ch[0].strip().lower() == "charset" and len(ch) > 1:
cha = ch[1].split(".")
if len(cha) > 1:
charset = cha[1]
else:
charset = cha[0]
except http.client.BadStatusLine:
raise IRCException("Invalid HTTP response")
finally:
conn.close()
if res.status == http.client.OK or res.status == http.client.SEE_OTHER:
return data.decode(charset)
elif ((res.status == http.client.FOUND or
res.status == http.client.MOVED_PERMANENTLY) and
res.getheader("Location") != url):
return getURLContent(res.getheader("Location"), timeout)
else:
raise IRCException("A HTTP error occurs: %d - %s" %
(res.status, http.client.responses[res.status]))
def getXML(url, timeout=15):
"""Get content page and return XML parsed content"""
cnt = getURLContent(url, timeout)
if cnt is None:
return None
else:
return parse_string(cnt.encode())
def getJSON(url, timeout=15):
"""Get content page and return JSON content"""
cnt = getURLContent(url, timeout)
if cnt is None:
return None
else:
return json.loads(cnt)
# Other utils
def htmlentitydecode(s):
"""Decode htmlentities"""
return re.sub('&(%s);' % '|'.join(name2codepoint),
lambda m: chr(name2codepoint[m.group(1)]), s)
def striphtml(data):
"""Remove HTML tags from text"""
p = re.compile(r'<.*?>')
return htmlentitydecode(p.sub('', data)
.replace("&#x28;", "/(")
.replace("&#x29;", ")/")
.replace("&#x22;", "\""))

View file

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import xml.sax
from . import node as module_state
logger = logging.getLogger("nemubot.tools.xmlparser")
class ModuleStatesFile(xml.sax.ContentHandler):
def startDocument(self):
self.root = None
self.stack = list()
def startElement(self, name, attrs):
cur = module_state.ModuleState(name)
for name in attrs.keys():
cur.setAttribute(name, attrs.getValue(name))
self.stack.append(cur)
def characters(self, content):
self.stack[len(self.stack)-1].content += content
def endElement(self, name):
child = self.stack.pop()
size = len(self.stack)
if size > 0:
self.stack[size - 1].content = self.stack[size - 1].content.strip()
self.stack[size - 1].addChild(child)
else:
self.root = child
def parse_file(filename):
parser = xml.sax.make_parser()
mod = ModuleStatesFile()
parser.setContentHandler(mod)
parser.parse(open(filename, "r"))
return mod.root
def parse_string(string):
mod = ModuleStatesFile()
xml.sax.parseString(string, mod)
return mod.root

View file

@ -0,0 +1,212 @@
# coding=utf-8
import xml.sax
from datetime import datetime, timezone
import logging
import time
logger = logging.getLogger("nemubot.tools.xmlparser.node")
class ModuleState:
"""Tiny tree representation of an XML file"""
def __init__(self, name):
self.name = name
self.content = ""
self.attributes = dict()
self.childs = list()
self.index = dict()
self.index_fieldname = None
self.index_tagname = None
def getName(self):
"""Get the name of the current node"""
return self.name
def display(self, level = 0):
ret = ""
out = list()
for k in self.attributes:
out.append("%s : %s" % (k, self.attributes[k]))
ret += "%s%s { %s } = '%s'\n" % (' ' * level, self.name,
' ; '.join(out), self.content)
for c in self.childs:
ret += c.display(level + 2)
return ret
def __str__(self):
return self.display()
def __getitem__(self, i):
"""Return the attribute asked"""
return self.getAttribute(i)
def __setitem__(self, i, c):
"""Set the attribute"""
return self.setAttribute(i, c)
def getAttribute(self, name):
"""Get the asked argument or return None if doesn't exist"""
if name in self.attributes:
return self.attributes[name]
else:
return None
def getDate(self, name=None):
"""Get the asked argument and return it as a date"""
if name is None:
source = self.content
elif name in self.attributes.keys():
source = self.attributes[name]
else:
return None
if isinstance(source, datetime):
return source
else:
try:
return datetime.fromtimestamp(float(source), timezone.utc)
except ValueError:
while True:
try:
return datetime.fromtimestamp(time.mktime(
time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")),
timezone.utc)
except ImportError:
pass
def getInt(self, name=None):
"""Get the asked argument and return it as an integer"""
if name is None:
source = self.content
elif name in self.attributes.keys():
source = self.attributes[name]
else:
return None
return int(float(source))
def getBool(self, name=None):
"""Get the asked argument and return it as an integer"""
if name is None:
source = self.content
elif name in self.attributes.keys():
source = self.attributes[name]
else:
return False
return (isinstance(source, bool) and source) or source == "True"
def tmpIndex(self, fieldname="name", tagname=None):
index = dict()
for child in self.childs:
if ((tagname is None or tagname == child.name) and
child.hasAttribute(fieldname)):
index[child[fieldname]] = child
return index
def setIndex(self, fieldname="name", tagname=None):
"""Defines an hash table to accelerate childs search.
You have just to define a common attribute"""
self.index = self.tmpIndex(fieldname, tagname)
self.index_fieldname = fieldname
self.index_tagname = tagname
def __contains__(self, i):
"""Return true if i is found in the index"""
if self.index:
return i in self.index
else:
return self.hasAttribute(i)
def hasAttribute(self, name):
"""DOM like method"""
return (name in self.attributes)
def setAttribute(self, name, value):
"""DOM like method"""
if (isinstance(value, datetime) or isinstance(value, str) or
isinstance(value, int) or isinstance(value, float)):
self.attributes[name] = value
else:
raise TypeError("attributes must be primary type "
"or datetime (here %s)" % type(value))
def getContent(self):
return self.content
def getChilds(self):
"""Return a full list of direct child of this node"""
return self.childs
def getNode(self, tagname):
"""Get a unique node (or the last one) with the given tagname"""
ret = None
for child in self.childs:
if tagname is None or tagname == child.name:
ret = child
return ret
def getFirstNode(self, tagname):
"""Get a unique node (or the last one) with the given tagname"""
for child in self.childs:
if tagname is None or tagname == child.name:
return child
return None
def getNodes(self, tagname):
"""Get all direct childs that have the given tagname"""
for child in self.childs:
if tagname is None or tagname == child.name:
yield child
def hasNode(self, tagname):
"""Return True if at least one node with the given tagname exists"""
for child in self.childs:
if tagname is None or tagname == child.name:
return True
return False
def addChild(self, child):
"""Add a child to this node"""
self.childs.append(child)
if self.index_fieldname is not None:
self.setIndex(self.index_fieldname, self.index_tagname)
def delChild(self, child):
"""Remove the given child from this node"""
self.childs.remove(child)
if self.index_fieldname is not None:
self.setIndex(self.index_fieldname, self.index_tagname)
def save_node(self, gen):
"""Serialize this node as a XML node"""
attribs = {}
for att in self.attributes.keys():
if att[0] != "_": # Don't save attribute starting by _
if isinstance(self.attributes[att], datetime):
attribs[att] = str(time.mktime(
self.attributes[att].timetuple()))
else:
attribs[att] = str(self.attributes[att])
attrs = xml.sax.xmlreader.AttributesImpl(attribs)
try:
gen.startElement(self.name, attrs)
for child in self.childs:
child.save_node(gen)
gen.endElement(self.name)
except:
logger.exception("Error occured when saving the following "
"XML node: %s with %s", self.name, attrs)
def save(self, filename):
"""Save the current node as root node in a XML file"""
with open(filename, "w") as f:
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
gen.startDocument()
self.save_node(gen)
gen.endDocument()