diff --git a/message.py b/message.py index 005948b..bd053d3 100644 --- a/message.py +++ b/message.py @@ -24,6 +24,7 @@ import time import credits from credits import Credits import DCC +from modules_keeper import loaded as mods import module_states_file as xmlparser CREDITS = {} @@ -157,7 +158,7 @@ class Message: return False return self.srv.accepted_channel(self.channel) - def treat(self, mods): + def treat(self): if self.cmd == "PING": self.pong () elif self.cmd == "PRIVMSG" and self.ctcp: @@ -197,7 +198,7 @@ class Message: elif self.content == '\x01USERINFO\x01': self.srv.send_ctcp(self.sender, "USERINFO %s" % (self.srv.realname)) elif self.content == '\x01VERSION\x01': - self.srv.send_ctcp(self.sender, "VERSION nemubot v3") + self.srv.send_ctcp(self.sender, "VERSION nemubot v%d"%VERSION) elif self.content[:9] == '\x01DCC CHAT': words = self.content[1:len(self.content) - 1].split(' ') ip = self.srv.toIP(int(words[3])) diff --git a/modules/velib.py b/modules/velib.py index 19ee264..72b53e4 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -6,13 +6,12 @@ from xml.dom.minidom import parseString from module_state import ModuleState -nemubotversion = 3.0 +nemubotversion = 3.1 def load(): global DATAS DATAS.setIndex("name", "station") - def help_tiny (): """Line inserted in the response to the command !help""" return "Gets information about velib stations" @@ -54,21 +53,20 @@ def station_status(msg, station): else: msg.send_chn("%s: station %s inconnue." % (msg.nick, station)) -def parseanswer(msg): +def checkStation(msg): global DATAS - if msg.cmd[0] == "velib": - if len(msg.cmd) > 5: - msg.send_chn("%s: Demande-moi moins de stations à la fois." % msg.nick) - elif len(msg.cmd) > 1: - for station in msg.cmd[1:]: - if re.match("^[0-9]{4,5}$", station): - station_status(msg, station) - elif station in DATAS.index: - station_status(msg, DATAS.index[station]["number"]) - else: - msg.send_chn("%s: numéro de station invalide." % (msg.nick)) - else: - msg.send_chn("%s: Pour quelle station ?" % msg.nick) - return True - else: + if len(msg.cmd) > 5: + msg.send_chn("%s: Demande-moi moins de stations à la fois." % msg.nick) return False + elif len(msg.cmd) > 1: + for station in msg.cmd[1:]: + if re.match("^[0-9]{4,5}$", station): + station_status(msg, station) + elif station in DATAS.index: + station_status(msg, DATAS.index[station]["number"]) + else: + msg.send_chn("%s: numéro de station invalide." % (msg.nick)) + else: + msg.send_chn("%s: Pour quelle station ?" % msg.nick) + return False + return True diff --git a/modules/velib.xml b/modules/velib.xml index dc9ca99..5350ac5 100644 --- a/modules/velib.xml +++ b/modules/velib.xml @@ -1,4 +1,14 @@ + Gets information about velib stations + + + !velib /number/ ... + gives available bikes and slots at the station /number/. + + + + + - \ No newline at end of file + diff --git a/nemubot.py b/nemubot.py index 3fac62c..789f08b 100755 --- a/nemubot.py +++ b/nemubot.py @@ -24,37 +24,48 @@ import traceback import reloader import prompt +import module_importer + +VERSION = 3.1 +module_importer.VERSION = VERSION +reloader.message.VERSION = VERSION + +datas_path = "./datas/" +prompt.datas_path = datas_path +module_importer.datas_path = datas_path servers = dict() -#Add modules dir path -if os.path.isdir("./modules/"): - modules_path = os.path.realpath(os.path.abspath("./modules/")) - if modules_path not in sys.path: - sys.path.insert(0, modules_path) +if __name__ == "__main__": + #Add modules dir path + if os.path.isdir("./modules/"): + modules_path = os.path.realpath(os.path.abspath("./modules/")) + if modules_path not in module_importer.modules_path: + module_importer.modules_path.append(modules_path + "/") -#Load given files -if len(sys.argv) >= 2: - for arg in sys.argv[1:]: - if os.path.isfile(arg): - prompt.load_file(arg, servers) - elif os.path.isdir(arg): - sys.path.insert(1, arg) + #Load given files + if len(sys.argv) >= 2: + for arg in sys.argv[1:]: + if os.path.isfile(arg): + prompt.load_file(arg, servers) + elif os.path.isdir(arg): + module_importer.modules_path.append(arg) -print ("Nemubot ready, my PID is %i!" % (os.getpid())) -while prompt.launch(servers): - try: - imp.reload(reloader) - if prompt.MODS is None: - reloader.reload() - else: - mods = prompt.MODS - reloader.reload() - prompt.MODS = mods - except: - print ("Unable to reload the prompt due to errors. Fix them before trying to reload the prompt.") - exc_type, exc_value, exc_traceback = sys.exc_info() - sys.stdout.write (traceback.format_exception_only(exc_type, exc_value)[0]) + print ("Nemubot ready, my PID is %i!" % (os.getpid())) + while prompt.launch(servers): + try: + imp.reload(reloader) + if module_importer.modules_loaded is None: + reloader.reload() + module_importer.modules_loaded = list() + else: + mods = module_importer.modules_loaded + reloader.reload() + module_importer.modules_loaded = mods + except: + print ("Unable to reload the prompt due to errors. Fix them before trying to reload the prompt.") + exc_type, exc_value, exc_traceback = sys.exc_info() + sys.stdout.write (traceback.format_exception_only(exc_type, exc_value)[0]) -print ("Bye") -sys.exit(0) + print ("Bye") + sys.exit(0) diff --git a/prompt.py b/prompt.py index fad092d..bc71dc0 100755 --- a/prompt.py +++ b/prompt.py @@ -16,20 +16,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import imp import os import shlex import sys import traceback import server +from modules_keeper import loaded as keeper import module_states_file as xmlparser selectedServer = None -modules_path = "./modules/" -datas_path = "./datas/" - -MODS = list() def parsecmd(msg): """Parse the command line""" @@ -60,17 +56,9 @@ def getPS1(): def launch(servers): """Launch the prompt and readline""" - global MODS - if MODS is None: - MODS = list() - #Load messages module server.message.load(datas_path + "general.xml") - #Update launched servers - for srv in servers: - servers[srv].update_mods(MODS) - ret = "" cmds = list() while ret != "quit" and ret != "reset" and ret != "refresh": @@ -98,131 +86,21 @@ def launch(servers): if ret == "refresh": return True #Save and shutdown modules - for m in MODS: - m.save() - try: - m.close() - except AttributeError: - pass - MODS = None + global keeper + for m in keeper: + keeper.unload_from_module(m) + keeper = None return ret == "reset" -########################## -# # -# Module functions # -# # -########################## - -def mod_save(mod, datas_path): - mod.DATAS.save(datas_path + "/" + mod.name + ".xml") - mod.print ("Saving!") - -def mod_has_access(mod, config, msg): - if config is not None and config.hasNode("channel"): - for chan in config.getNodes("channel"): - if (chan["server"] is None or chan["server"] == msg.srv.id) and (chan["channel"] is None or chan["channel"] == msg.channel): - return True - return False - else: - return True - ########################## # # # Permorming functions # # # ########################## -def load_module_from_name(name, servers, config=None): - try: - #Import the module code - loaded = False - for md in MODS: - if md.name == name: - mod = imp.reload(md) - loaded = True - if hasattr(mod, 'reload'): - mod.reload() - break - if not loaded: - mod = __import__(name) - imp.reload(mod) - try: - if mod.nemubotversion < 3.0: - print (" Module `%s' is not compatible with this version." % name) - return false - - #Set module common functions and datas - mod.name = name - mod.print = lambda msg: print("[%s] %s"%(mod.name, msg)) - mod.DATAS = xmlparser.parse_file(datas_path + "/" + name + ".xml") - mod.CONF = config - mod.SRVS = servers - mod.has_access = lambda msg: mod_has_access(mod, config, msg) - mod.save = lambda: mod_save(mod, datas_path) - - #Load dependancies - if mod.CONF is not None and mod.CONF.hasNode("dependson"): - mod.MODS = dict() - for depend in mod.CONF.getNodes("dependson"): - for md in MODS: - if md.name == depend["name"]: - mod.MODS[md.name] = md - break - if depend["name"] not in mod.MODS: - print ("\033[1;31mERROR:\033[0m in module `%s', module `%s' require by this module but is not loaded." % (mod.name,depend["name"])) - return - - try: - test = mod.parseask - except AttributeError: - print ("\033[1;35mWarning:\033[0m in module `%s', no function parseask defined." % mod.name) - mod.parseask = lambda x: False - - try: - test = mod.parseanswer - except AttributeError: - print ("\033[1;35mWarning:\033[0m in module `%s', no function parseanswer defined." % mod.name) - mod.parseanswer = lambda x: False - - try: - test = mod.parselisten - except AttributeError: - print ("\033[1;35mWarning:\033[0m in module `%s', no function parselisten defined." % mod.name) - mod.parselisten = lambda x: False - - try: - mod.load() - print (" Module `%s' successfully loaded." % name) - except AttributeError: - print (" Module `%s' successfully added." % name) - #TODO: don't append already running modules - exitsts = False - for md in MODS: - if md.name == name: - exitsts = True - break - if not exitsts: - MODS.append(mod) - except AttributeError : - print (" Module `%s' is not a nemubot module." % name) - for srv in servers: - servers[srv].update_mods(MODS) - except IOError: - print (" Module `%s' not loaded: unable to find module implementation." % name) - except ImportError: - print ("\033[1;31mERROR:\033[0m Module attached to the file `%s' not loaded. Is this file existing?" % name) - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, exc_traceback) - -def load_module(config, servers): - global MODS - if config.hasAttribute("name"): - load_module_from_name(config["name"], servers, config) - def load_file(filename, servers): """Realy load a file""" - global MODS if os.path.isfile(filename): config = xmlparser.parse_file(filename) if config.getName() == "nemubotconfig" or config.getName() == "config": @@ -235,20 +113,16 @@ def load_file(filename, servers): else: print (" Server `%s' already added, skiped." % srv.id) if srv.autoconnect: - srv.launch(MODS) + srv.launch() #Load files asked by the configuration file for load in config.getNodes("load"): load_file(load["path"], servers) elif config.getName() == "nemubotmodule": - load_module(config, servers) + __import__(config["name"]) else: print (" Can't load `%s'; this is not a valid nemubot configuration file." % filename) - elif os.path.isfile(filename + ".xml"): - load_file(filename + ".xml", servers) - elif os.path.isfile("./modules/" + filename + ".xml"): - load_file("./modules/" + filename + ".xml", servers) else: - load_module_from_name(filename, servers) + __import__(filename) def load(cmds, servers): @@ -261,18 +135,16 @@ def load(cmds, servers): return def unload(cmds, servers): - """Unload a module""" - global MODS - if len(cmds) == 2 and cmds[1] == "all": - for mod in MODS: - try: - mod.unload() - except AttributeError: - continue - while len(MODS) > 0: - MODS.pop() - elif len(cmds) > 1: - print("Ok") + """Unload a module""" + if len(cmds) == 2 and cmds[1] == "all": + for mod in MODS: + keeper.unload_from_module(mod.name) + elif len(cmds) > 1: + for name in cmds[1:]: + if keeper.unload_from_name(name): + print(" Module `%s' successfully unloaded." % name) + else: + print(" No module `%s' loaded, can't unload!" % name) def close(cmds, servers): """Disconnect and forget (remove from the servers list) the server""" @@ -311,7 +183,7 @@ def liste(cmds, servers): for srv in servers.keys(): print (" - %s ;" % srv) elif l == "mod" or l == "mods" or l == "module" or l == "modules": - for mod in MODS: + for mod in keeper: print (" - %s ;" % mod.name) elif l == "ban" or l == "banni": for ban in server.message.BANLIST: @@ -335,24 +207,23 @@ def connect(cmds, servers): if len(cmds) > 1: for s in cmds[1:]: if s in servers: - servers[s].launch(MODS) + servers[s].launch() else: print ("connect: server `%s' not found." % s) elif selectedServer is not None: - selectedServer.launch(MODS) + selectedServer.launch() else: print (" Please SELECT a server or give its name in argument.") def hotswap(cmds, servers): """Reload a server class""" - global MODS, selectedServer + global selectedServer if len(cmds) > 1: print ("hotswap: apply only on selected server") elif selectedServer is not None: del servers[selectedServer.id] srv = server.Server(selectedServer.node, selectedServer.nick, selectedServer.owner, selectedServer.realname, selectedServer.s) - srv.update_mods(MODS) servers[srv.id] = srv selectedServer.kill() selectedServer = srv diff --git a/reloader.py b/reloader.py index b3c6465..6c1ee05 100644 --- a/reloader.py +++ b/reloader.py @@ -22,6 +22,7 @@ import credits import channel import DCC import message +import module_importer import module_state import module_states_file import prompt @@ -32,6 +33,7 @@ def reload(): imp.reload(channel) imp.reload(DCC) imp.reload(message) + imp.reload(module_importer) imp.reload(module_state) imp.reload(module_states_file) imp.reload(prompt) diff --git a/server.py b/server.py deleted file mode 100644 index 1dc5bf5..0000000 --- a/server.py +++ /dev/null @@ -1,293 +0,0 @@ -# -*- 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 . - -import socket -import sys -import threading -import traceback - -import channel -import DCC -import message -import module_states_file as xmlparser - -class Server(threading.Thread): - def __init__(self, node, nick, owner, realname, socket = None): - self.stop = False - self.stopping = threading.Event() - self.nick = nick - self.owner = owner - self.realname = realname - self.s = socket - self.connected = self.s is not None - self.node = node - - self.listen_nick = True - - self.dcc_clients = dict() - - self.channels = dict() - for chn in node.getNodes("channel"): - chan = channel.Channel(chn, self) - self.channels[chan.name] = chan - - threading.Thread.__init__(self) - - def isDCC(self, to=None): - return to is not None and to in self.dcc_clients - - @property - def host(self): - if self.node.hasAttribute("server"): - return self.node["server"] - else: - return "localhost" - - @property - def port(self): - if self.node.hasAttribute("port"): - return self.node.getInt("port") - else: - return "6667" - - @property - def password(self): - if self.node.hasAttribute("password"): - return self.node["password"] - else: - return None - - @property - def partner(self): - if self.node.hasAttribute("partner"): - return self.node["partner"] - else: - return None - - @property - def ip(self): - """Convert common IP representation to little-endian integer representation""" - sum = 0 - if self.node.hasAttribute("ip"): - for b in self.node["ip"].split("."): - sum = 256 * sum + int(b) - else: - #TODO: find the external IP - pass - return sum - - def toIP(self, input): - """Convert little-endian int to IPv4 adress""" - ip = "" - for i in range(0,4): - mod = input % 256 - ip = "%d.%s" % (mod, ip) - input = (input - mod) / 256 - return ip[:len(ip) - 1] - - @property - def autoconnect(self): - if self.node.hasAttribute("autoconnect"): - value = self.node["autoconnect"].lower() - return value != "no" and value != "off" and value != "false" - else: - return False - - @property - def id(self): - return self.host + ":" + str(self.port) - - def send_ctcp(self, to, msg, cmd = "NOTICE", endl = "\r\n"): - """Send a message as CTCP response""" - if msg is not None and to is not None: - for line in msg.split("\n"): - if line != "": - self.s.send (("%s %s :\x01%s\x01%s" % (cmd, to.split("!")[0], line, endl)).encode ()) - - def send_dcc(self, msg, to): - """Send a message through DCC connection""" - if msg is not None and to is not None: - realname = to.split("!")[1] - if realname not in self.dcc_clients.keys(): - d = dcc.DCC(self, to) - self.dcc_clients[realname] = d - self.dcc_clients[realname].send_dcc(msg) - - - def send_msg_final(self, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): - """Send a message without checks""" - if channel == self.nick: - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, exc_traceback) - print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg) - if msg is not None and channel is not None: - for line in msg.split("\n"): - if line != "": - if len(line) < 442: - self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) - else: - self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ()) - - def send_msg_prtn(self, msg): - """Send a message to partner bot""" - self.send_msg_final(self.partner, msg) - - def send_msg_usr(self, user, msg): - """Send a message to a user instead of a channel""" - if user is not None and user[0] != "#": - realname = user.split("!")[1] - if realname in self.dcc_clients: - self.send_dcc(msg, user) - else: - self.send_msg_final(user.split('!')[0], msg) - - def send_msg(self, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): - """Send a message to a channel""" - if self.accepted_channel(channel): - self.send_msg_final(channel, msg, cmd, endl) - - def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): - """Send a message to a channel, only if the source user is on this channel too""" - if self.accepted_channel(channel, sender): - self.send_msg_final(channel, msg, cmd, endl) - - def send_global(self, msg, cmd = "PRIVMSG", endl = "\r\n"): - """Send a message to all channels on this server""" - for channel in self.channels.keys(): - self.send_msg(channel, msg, cmd, endl) - - - def accepted_channel(self, chan, sender = None): - """Return True if the channel (or the user) is authorized""" - if self.listen_nick: - return (chan in self.channels and (sender is None or sender in self.channels[chan].people)) or chan == self.nick - else: - return chan in self.channels and (sender is None or sender in self.channels[chan].people) - - def disconnect(self): - """Close the socket with the server and all DCC client connections""" - if self.connected: - self.stop = True - self.s.shutdown(socket.SHUT_RDWR) - - #Close all DCC connection - for clt in self.dcc_clients: - self.dcc_clients[clt].disconnect() - - self.stopping.wait() - return True - else: - return False - - def kill(self): - if self.connected: - self.stop = True - self.connected = False - #Send a message in order to close the socket - self.s.send(("WHO %s\r\n" % self.nick).encode ()) - self.stopping.wait() - return True - else: - return False - - def join(self, chan, password = None): - """Join a channel""" - if chan is not None and self.connected and chan not in self.channels: - chn = xmlparser.module_state.ModuleState("channel") - chn["name"] = chan - chn["password"] = password - self.node.addChild(chn) - self.channels[chan] = channel.Channel(chn, self) - if password is not None: - self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode ()) - else: - self.s.send(("JOIN %s\r\n" % chan).encode ()) - return True - else: - return False - - def leave(self, chan): - """Leave a channel""" - if chan is not None and self.connected and chan in self.channels: - self.s.send(("PART %s\r\n" % self.channels[chan].name).encode ()) - del self.channels[chan] - return True - else: - return False - - def update_mods(self, mods): - self.mods = mods - - def launch(self, mods): - """Connect to the server if it is no yet connected""" - if not self.connected: - self.stop = False - self.mods = mods - self.start() - else: - print (" Already connected.") - - def treat_msg(self, line, private = False): - try: - msg = message.Message (self, line, private) - msg.treat (self.mods) - except: - print ("\033[1;31mERROR:\033[0m occurred during the processing of the message: %s" % line) - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, exc_traceback) - - def run(self): - if not self.connected: - self.s = socket.socket() #Create the socket - self.s.connect((self.host, self.port)) #Connect to server - self.stopping.clear() - self.connected = True - - if self.password != None: - self.s.send(b"PASS " + self.password.encode () + b"\r\n") - self.s.send(("NICK %s\r\n" % self.nick).encode ()) - self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host, self.realname)).encode ()) - print ("Connection to %s:%d completed" % (self.host, self.port)) - - if len(self.channels) > 0: - for chn in self.channels.keys(): - if self.channels[chn].password is not None: - self.s.send(("JOIN %s %s\r\n" % (self.channels[chn].name, self.channels[chn].password)).encode ()) - else: - self.s.send(("JOIN %s\r\n" % self.channels[chn].name).encode ()) - print ("Listen to channels: %s" % ' '.join (self.channels.keys())) - - readbuffer = b'' #Here we store all the messages from server - while not self.stop: - raw = self.s.recv(1024) #recieve server messages - if not raw: - break - readbuffer = readbuffer + raw - temp = readbuffer.split(b'\n') - readbuffer = temp.pop() - - for line in temp: - self.treat_msg(line) - - if self.connected: - self.s.close() - self.connected = False - print ("Server `%s' successfully stopped." % self.id) - self.stopping.set() - #Rearm Thread - threading.Thread.__init__(self)