Working copy of v3.1

This commit is contained in:
Némunaire 2012-11-04 17:09:54 +01:00
parent afdf951758
commit e2fbdecc59
7 changed files with 93 additions and 493 deletions

View File

@ -24,6 +24,7 @@ import time
import credits import credits
from credits import Credits from credits import Credits
import DCC import DCC
from modules_keeper import loaded as mods
import module_states_file as xmlparser import module_states_file as xmlparser
CREDITS = {} CREDITS = {}
@ -157,7 +158,7 @@ class Message:
return False return False
return self.srv.accepted_channel(self.channel) return self.srv.accepted_channel(self.channel)
def treat(self, mods): def treat(self):
if self.cmd == "PING": if self.cmd == "PING":
self.pong () self.pong ()
elif self.cmd == "PRIVMSG" and self.ctcp: elif self.cmd == "PRIVMSG" and self.ctcp:
@ -197,7 +198,7 @@ class Message:
elif self.content == '\x01USERINFO\x01': elif self.content == '\x01USERINFO\x01':
self.srv.send_ctcp(self.sender, "USERINFO %s" % (self.srv.realname)) self.srv.send_ctcp(self.sender, "USERINFO %s" % (self.srv.realname))
elif self.content == '\x01VERSION\x01': 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': elif self.content[:9] == '\x01DCC CHAT':
words = self.content[1:len(self.content) - 1].split(' ') words = self.content[1:len(self.content) - 1].split(' ')
ip = self.srv.toIP(int(words[3])) ip = self.srv.toIP(int(words[3]))

View File

@ -6,13 +6,12 @@ from xml.dom.minidom import parseString
from module_state import ModuleState from module_state import ModuleState
nemubotversion = 3.0 nemubotversion = 3.1
def load(): def load():
global DATAS global DATAS
DATAS.setIndex("name", "station") DATAS.setIndex("name", "station")
def help_tiny (): def help_tiny ():
"""Line inserted in the response to the command !help""" """Line inserted in the response to the command !help"""
return "Gets information about velib stations" return "Gets information about velib stations"
@ -54,11 +53,11 @@ def station_status(msg, station):
else: else:
msg.send_chn("%s: station %s inconnue." % (msg.nick, station)) msg.send_chn("%s: station %s inconnue." % (msg.nick, station))
def parseanswer(msg): def checkStation(msg):
global DATAS global DATAS
if msg.cmd[0] == "velib":
if len(msg.cmd) > 5: if len(msg.cmd) > 5:
msg.send_chn("%s: Demande-moi moins de stations à la fois." % msg.nick) msg.send_chn("%s: Demande-moi moins de stations à la fois." % msg.nick)
return False
elif len(msg.cmd) > 1: elif len(msg.cmd) > 1:
for station in msg.cmd[1:]: for station in msg.cmd[1:]:
if re.match("^[0-9]{4,5}$", station): if re.match("^[0-9]{4,5}$", station):
@ -69,6 +68,5 @@ def parseanswer(msg):
msg.send_chn("%s: numéro de station invalide." % (msg.nick)) msg.send_chn("%s: numéro de station invalide." % (msg.nick))
else: else:
msg.send_chn("%s: Pour quelle station ?" % msg.nick) msg.send_chn("%s: Pour quelle station ?" % msg.nick)
return True
else:
return False return False
return True

View File

@ -1,4 +1,14 @@
<?xml version="1.0" ?> <?xml version="1.0" ?>
<nemubotmodule name="velib"> <nemubotmodule name="velib">
<help>Gets information about velib stations</help>
<command name="velib" call="checkStation">
<usage>!velib /number/ ...</usage>
<help>gives available bikes and slots at the station /number/.</help>
</command>
<event name="velib" call="nothing">
</event>
<server ip="www.velib.paris.fr" url="/service/stationdetails/paris/" /> <server ip="www.velib.paris.fr" url="/service/stationdetails/paris/" />
</nemubotmodule> </nemubotmodule>

View File

@ -24,37 +24,48 @@ import traceback
import reloader import reloader
import prompt 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() servers = dict()
#Add modules dir path if __name__ == "__main__":
if os.path.isdir("./modules/"): #Add modules dir path
if os.path.isdir("./modules/"):
modules_path = os.path.realpath(os.path.abspath("./modules/")) modules_path = os.path.realpath(os.path.abspath("./modules/"))
if modules_path not in sys.path: if modules_path not in module_importer.modules_path:
sys.path.insert(0, modules_path) module_importer.modules_path.append(modules_path + "/")
#Load given files #Load given files
if len(sys.argv) >= 2: if len(sys.argv) >= 2:
for arg in sys.argv[1:]: for arg in sys.argv[1:]:
if os.path.isfile(arg): if os.path.isfile(arg):
prompt.load_file(arg, servers) prompt.load_file(arg, servers)
elif os.path.isdir(arg): elif os.path.isdir(arg):
sys.path.insert(1, arg) module_importer.modules_path.append(arg)
print ("Nemubot ready, my PID is %i!" % (os.getpid())) print ("Nemubot ready, my PID is %i!" % (os.getpid()))
while prompt.launch(servers): while prompt.launch(servers):
try: try:
imp.reload(reloader) imp.reload(reloader)
if prompt.MODS is None: if module_importer.modules_loaded is None:
reloader.reload() reloader.reload()
module_importer.modules_loaded = list()
else: else:
mods = prompt.MODS mods = module_importer.modules_loaded
reloader.reload() reloader.reload()
prompt.MODS = mods module_importer.modules_loaded = mods
except: except:
print ("Unable to reload the prompt due to errors. Fix them before trying to reload the prompt.") 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() exc_type, exc_value, exc_traceback = sys.exc_info()
sys.stdout.write (traceback.format_exception_only(exc_type, exc_value)[0]) sys.stdout.write (traceback.format_exception_only(exc_type, exc_value)[0])
print ("Bye") print ("Bye")
sys.exit(0) sys.exit(0)

165
prompt.py
View File

@ -16,20 +16,16 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import imp
import os import os
import shlex import shlex
import sys import sys
import traceback import traceback
import server import server
from modules_keeper import loaded as keeper
import module_states_file as xmlparser import module_states_file as xmlparser
selectedServer = None selectedServer = None
modules_path = "./modules/"
datas_path = "./datas/"
MODS = list()
def parsecmd(msg): def parsecmd(msg):
"""Parse the command line""" """Parse the command line"""
@ -60,17 +56,9 @@ def getPS1():
def launch(servers): def launch(servers):
"""Launch the prompt and readline""" """Launch the prompt and readline"""
global MODS
if MODS is None:
MODS = list()
#Load messages module #Load messages module
server.message.load(datas_path + "general.xml") server.message.load(datas_path + "general.xml")
#Update launched servers
for srv in servers:
servers[srv].update_mods(MODS)
ret = "" ret = ""
cmds = list() cmds = list()
while ret != "quit" and ret != "reset" and ret != "refresh": while ret != "quit" and ret != "reset" and ret != "refresh":
@ -98,131 +86,21 @@ def launch(servers):
if ret == "refresh": if ret == "refresh":
return True return True
#Save and shutdown modules #Save and shutdown modules
for m in MODS: global keeper
m.save() for m in keeper:
try: keeper.unload_from_module(m)
m.close() keeper = None
except AttributeError:
pass
MODS = None
return ret == "reset" 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 # # 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): def load_file(filename, servers):
"""Realy load a file""" """Realy load a file"""
global MODS
if os.path.isfile(filename): if os.path.isfile(filename):
config = xmlparser.parse_file(filename) config = xmlparser.parse_file(filename)
if config.getName() == "nemubotconfig" or config.getName() == "config": if config.getName() == "nemubotconfig" or config.getName() == "config":
@ -235,20 +113,16 @@ def load_file(filename, servers):
else: else:
print (" Server `%s' already added, skiped." % srv.id) print (" Server `%s' already added, skiped." % srv.id)
if srv.autoconnect: if srv.autoconnect:
srv.launch(MODS) srv.launch()
#Load files asked by the configuration file #Load files asked by the configuration file
for load in config.getNodes("load"): for load in config.getNodes("load"):
load_file(load["path"], servers) load_file(load["path"], servers)
elif config.getName() == "nemubotmodule": elif config.getName() == "nemubotmodule":
load_module(config, servers) __import__(config["name"])
else: else:
print (" Can't load `%s'; this is not a valid nemubot configuration file." % filename) 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: else:
load_module_from_name(filename, servers) __import__(filename)
def load(cmds, servers): def load(cmds, servers):
@ -262,17 +136,15 @@ def load(cmds, servers):
def unload(cmds, servers): def unload(cmds, servers):
"""Unload a module""" """Unload a module"""
global MODS
if len(cmds) == 2 and cmds[1] == "all": if len(cmds) == 2 and cmds[1] == "all":
for mod in MODS: for mod in MODS:
try: keeper.unload_from_module(mod.name)
mod.unload()
except AttributeError:
continue
while len(MODS) > 0:
MODS.pop()
elif len(cmds) > 1: elif len(cmds) > 1:
print("Ok") 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): def close(cmds, servers):
"""Disconnect and forget (remove from the servers list) the server""" """Disconnect and forget (remove from the servers list) the server"""
@ -311,7 +183,7 @@ def liste(cmds, servers):
for srv in servers.keys(): for srv in servers.keys():
print (" - %s ;" % srv) print (" - %s ;" % srv)
elif l == "mod" or l == "mods" or l == "module" or l == "modules": elif l == "mod" or l == "mods" or l == "module" or l == "modules":
for mod in MODS: for mod in keeper:
print (" - %s ;" % mod.name) print (" - %s ;" % mod.name)
elif l == "ban" or l == "banni": elif l == "ban" or l == "banni":
for ban in server.message.BANLIST: for ban in server.message.BANLIST:
@ -335,24 +207,23 @@ def connect(cmds, servers):
if len(cmds) > 1: if len(cmds) > 1:
for s in cmds[1:]: for s in cmds[1:]:
if s in servers: if s in servers:
servers[s].launch(MODS) servers[s].launch()
else: else:
print ("connect: server `%s' not found." % s) print ("connect: server `%s' not found." % s)
elif selectedServer is not None: elif selectedServer is not None:
selectedServer.launch(MODS) selectedServer.launch()
else: else:
print (" Please SELECT a server or give its name in argument.") print (" Please SELECT a server or give its name in argument.")
def hotswap(cmds, servers): def hotswap(cmds, servers):
"""Reload a server class""" """Reload a server class"""
global MODS, selectedServer global selectedServer
if len(cmds) > 1: if len(cmds) > 1:
print ("hotswap: apply only on selected server") print ("hotswap: apply only on selected server")
elif selectedServer is not None: elif selectedServer is not None:
del servers[selectedServer.id] del servers[selectedServer.id]
srv = server.Server(selectedServer.node, selectedServer.nick, selectedServer.owner, selectedServer.realname, selectedServer.s) srv = server.Server(selectedServer.node, selectedServer.nick, selectedServer.owner, selectedServer.realname, selectedServer.s)
srv.update_mods(MODS)
servers[srv.id] = srv servers[srv.id] = srv
selectedServer.kill() selectedServer.kill()
selectedServer = srv selectedServer = srv

View File

@ -22,6 +22,7 @@ import credits
import channel import channel
import DCC import DCC
import message import message
import module_importer
import module_state import module_state
import module_states_file import module_states_file
import prompt import prompt
@ -32,6 +33,7 @@ def reload():
imp.reload(channel) imp.reload(channel)
imp.reload(DCC) imp.reload(DCC)
imp.reload(message) imp.reload(message)
imp.reload(module_importer)
imp.reload(module_state) imp.reload(module_state)
imp.reload(module_states_file) imp.reload(module_states_file)
imp.reload(prompt) imp.reload(prompt)

293
server.py
View File

@ -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 <http://www.gnu.org/licenses/>.
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)