Introduce nemubot v3.2

- New licence: AGPL3 instead of GPL3
	- Import is now based on finder and loader instead of sys.path
	- Modules used hooks to treat message instead of treating all messages
	- Remove a lot of builtins from the prompt
	- Prompt: ^C and ^D have now correct feature (nothing and exit)
This commit is contained in:
Némunaire 2012-08-14 05:51:55 +02:00
parent a2b273d09b
commit e4d4e68c45
11 changed files with 964 additions and 177 deletions

141
COPYING
View File

@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
@ -7,17 +7,15 @@
Preamble Preamble
The GNU General Public License is a free, copyleft license for The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works. software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast, to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the software for all its users.
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you price. Our General Public Licenses are designed to make sure that you
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things. free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you Developers that use our General Public Licenses protect your rights
these rights or asking you to surrender the rights. Therefore, you have with two steps: (1) assert copyright on the software, and (2) offer
certain responsibilities if you distribute copies of the software, or if you this License which gives you legal permission to copy, distribute
you modify it: responsibilities to respect the freedom of others. and/or modify the software.
For example, if you distribute copies of such a program, whether A secondary benefit of defending all users' freedom is that
gratis or for a fee, you must pass on to the recipients the same improvements made in alternate versions of the program, if they
freedoms that you received. You must make sure that they, too, receive receive widespread use, become available for other developers to
or can get the source code. And you must show them these terms so they incorporate. Many developers of free software are heartened and
know their rights. encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps: The GNU Affero General Public License is designed specifically to
(1) assert copyright on the software, and (2) offer you this License ensure that, in such cases, the modified source code becomes available
giving you legal permission to copy, distribute and/or modify it. to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains An older license, called the Affero General Public License and
that there is no warranty for this free software. For both users' and published by Affero, was designed to accomplish similar goals. This is
authors' sake, the GPL requires that modified versions be marked as a different license, not a version of the Affero GPL, but Affero has
changed, so that their problems will not be attributed erroneously to released a new version of the Affero GPL which permits relicensing under
authors of previous versions. this license.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
@ -72,7 +60,7 @@ modification follow.
0. Definitions. 0. Definitions.
"This License" refers to version 3 of the GNU General Public License. "This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks. works, such as semiconductor masks.
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program. License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License. 13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work, License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License, but the work with which it is combined will remain governed by version
section 13, concerning interaction through a network will apply to the 3 of the GNU General Public License.
combination as such.
14. Revised Versions of this License. 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will the GNU Affero General Public License from time to time. Such new versions
be similar in spirit to the present version, but may differ in detail to will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns. address new problems or concerns.
Each version is given a distinguishing version number. If the Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation. by the Free Software Foundation.
If the Program specifies that a proxy can decide which future If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you public statement of acceptance of a version permanently authorizes you
to choose that version for the Program. to choose that version for the Program.
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author> Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by 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 the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU 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/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If your software can interact with users remotely through a computer
notice like this when it starts in an interactive mode: network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
<program> Copyright (C) <year> <name of author> interface could display a "Source" link that leads users to an archive
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. of the code. There are many ways you could offer source, and different
This is free software, and you are welcome to redistribute it solutions will be better for different programs; see section 13 for the
under certain conditions; type `show c' for details. specific requirements.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

129
bot.py Normal file
View File

@ -0,0 +1,129 @@
# -*- 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 hooks
from server import Server
class Bot:
def __init__(self, servers=dict(), modules=dict(), mp=list()):
self.version = 3.2
self.version_txt = "3.2"
self.servers = servers
self.modules = modules
self.modules_path = mp
self.datas_path = './datas/'
self.hooks = hooks.MessagesHook()
def addServer(self, node, nick, owner, realname):
"""Add a new server to the context"""
srv = Server(node, nick, owner, realname)
if srv.id not in self.servers:
self.servers[srv.id] = srv
if srv.autoconnect:
srv.launch(self)
return True
else:
return False
def add_module(self, module):
"""Add a module to the context, if already exists, unload the
old one before"""
# Check if the module already exists
for mod in self.modules.keys():
if self.modules[mod].name == module.name:
self.unload_module(self.modules[mod].name)
break
self.modules[module.name] = module
return True
def add_modules_path(self, path):
"""Add a path to the modules_path array, used by module loader"""
# The path must end by / char
if path[len(path)-1] != "/":
path = path + "/"
if path not in self.modules_path:
self.modules_path.append(path)
return True
return False
def unload_module(self, name, verb=False):
"""Unload a module"""
if name in self.modules:
self.modules[name].save()
if hasattr(self.modules[name], "unload"):
self.modules[name].unload()
# Remove from the dict
del self.modules[name]
return True
return False
def quit(self, verb=False):
"""Save and unload modules and disconnect servers"""
if verb: print ("Save and unload all modules...")
k = list(self.modules.keys())
for mod in k:
print (mod)
self.unload_module(mod, verb)
if verb: print ("Close all servers connection...")
k = list(self.servers.keys())
for srv in k:
self.servers[srv].disconnect()
def hotswap(bak):
return Bot(bak.servers, bak.modules, bak.modules_path)
def reload():
import imp
import prompt.builtins
imp.reload(prompt.builtins)
import hooks
imp.reload(hooks)
import xmlparser
imp.reload(xmlparser)
import xmlparser.node
imp.reload(xmlparser.node)
import importer
imp.reload(importer)
import server
imp.reload(server)
import channel
imp.reload(channel)
import DCC
imp.reload(DCC)
import message
imp.reload(message)

151
hooks.py Normal file
View File

@ -0,0 +1,151 @@
# -*- 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/>.
class MessagesHook:
def __init__(self):
# Store direct hook
self.cmd_hook = dict()
self.ask_hook = dict()
self.msg_hook = dict()
# Store regexp hook
self.cmd_rgxp = list()
self.ask_rgxp = list()
self.msg_rgxp = list()
def add_hook(self, store, hook):
"""Insert in the right place a hook into the given store"""
if isinstance(store, dict) and hook.name is not None:
if hook.name not in store:
store[hook.name] = list()
store[hook.name].append(hook)
elif isinstance(store, list):
store.append(hook)
else:
print ("Warning: unrecognized hook store type")
def register_hook(self, module, node):
"""Create a hook from configuration node"""
if node.name == "message" and node.hasAttribute("type"):
if node["type"] == "cmd" or node["type"] == "all":
if node.hasAttribute("name"):
self.add_hook(self.cmd_hook, Hook(getattr(module,
node["call"]),
node["name"]))
elif node.hasAttribute("regexp"):
self.add_hook(self.cmd_rgxp, Hook(getattr(module,
node["call"]),
None, None,
node["regexp"]))
if node["type"] == "ask" or node["type"] == "all":
if node.hasAttribute("name"):
self.add_hook(self.ask_hook, Hook(getattr(module,
node["call"]),
node["name"]))
elif node.hasAttribute("regexp"):
self.add_hook(self.ask_rgxp, Hook(getattr(module,
node["call"]),
None, None,
node["regexp"]))
if node["type"] == "answer" or node["type"] == "all":
if node.hasAttribute("name"):
self.add_hook(self.msg_hook, Hook(getattr(module,
node["call"]),
node["name"]))
elif node.hasAttribute("regexp"):
self.add_hook(self.msg_rgxp, Hook(getattr(module,
node["call"]),
None, None,
node["regexp"]))
def check_rest_times(self, store, hook):
"""Remove from store the hook if it has been executed given time"""
if hook.times == 0:
if isinstance(store, dict):
store[hook.name].remove(hook)
if len(store) == 0:
del store[hook.name]
elif isinstance(store, list):
store.remove(hook)
def treat_cmd(self, msg):
"""Treat a command message"""
# First, treat simple hook
if msg.cmd[0] in self.cmd_hook:
for h in self.cmd_hook[msg.cmd[0]]:
h.run(msg)
self.check_rest_times(self.cmd_hook, h)
# Then, treat regexp based hook
for hook in self.cmd_rgxp:
if hook.is_matching(msg):
hook.run(msg)
self.check_rest_times(self.cmd_rgxp, hook)
def treat_ask(self, msg):
"""Treat an ask message"""
# First, treat simple hook
if msg.content in self.ask_hook:
for h in self.ask_hook[msg.content]:
h.run(msg)
self.check_rest_times(self.ask_hook, h)
# Then, treat regexp based hook
for hook in self.ask_rgxp:
if hook.is_matching(msg):
hook.run(msg)
self.check_rest_times(self.ask_rgxp, hook)
def treat_answer(self, msg):
"""Treat a normal message"""
# First, treat simple hook
if msg.content in self.msg_hook:
for h in self.msg_hook[msg.cmd[0]]:
h.run(msg)
self.check_rest_times(self.msg_hook, h)
# Then, treat regexp based hook
for hook in self.msg_rgxp:
if hook.is_matching(msg):
hook.run(msg)
self.check_rest_times(self.msg_rgxp, hook)
class Hook:
"""Class storing hook informations"""
def __init__(self, call, name=None, data=None, regexp=None):
self.name = name
self.call = call
self.regexp = regexp
self.data = data
self.times = -1
def is_matching(self, strcmp):
"""Test if the current hook correspond to the message"""
return (self.name is not None and strcmp == self.name) or (
self.regexp is not None and re.match(self.regexp, strcmp))
def run(self, msg):
"""Run the hook"""
if self.times > 0:
self.times -= 1
return self.call(self.data, msg)

229
importer.py Normal file
View File

@ -0,0 +1,229 @@
# -*- 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 importlib.abc import Finder
from importlib.abc import SourceLoader
import imp
import os
import sys
import xmlparser
class ModuleFinder(Finder):
def __init__(self, context, prompt):
self.context = context
self.prompt = prompt
def find_module(self, fullname, path=None):
#print ("looking for", fullname, "in", path)
# Search only for new nemubot modules (packages init)
if path is None:
for mpath in self.context.modules_path:
#print ("looking for", fullname, "in", mpath)
if os.path.isfile(mpath + fullname + ".xml"):
return ModuleLoader(self.context, self.prompt, fullname,
mpath, mpath + fullname + ".xml")
elif (os.path.isfile(mpath + fullname + ".py") or
os.path.isfile(mpath + fullname + "/__init__.py")):
return ModuleLoader(self.context, self.prompt,
fullname, mpath, None)
#print ("not found")
return None
class ModuleLoader(SourceLoader):
def __init__(self, context, prompt, fullname, path, config_path):
self.context = context
self.prompt = prompt
self.name = fullname
self.config_path = config_path
if config_path is not None:
self.config = xmlparser.parse_file(config_path)
if self.config.hasAttribute("name"):
self.name = self.config["name"]
if os.path.isfile(path + fullname + ".py"):
self.source_path = path + self.name + ".py"
self.package = False
elif os.path.isfile(path + fullname + "/__init__.py"):
self.source_path = path + self.name + "/__init__.py"
self.package = True
else:
raise ImportError
def get_filename(self, fullname):
"""Return the path to the source file as found by the finder."""
return self.source_path
def get_data(self, path):
"""Return the data from path as raw bytes."""
with open(path, 'rb') as file:
return file.read()
def path_mtime(self, path):
st = os.stat(path)
return int(st.st_mtime)
def set_data(self, path, data):
"""Write bytes data to a file."""
parent, filename = os.path.split(path)
path_parts = []
# Figure out what directories are missing.
while parent and not os.path.isdir(parent):
parent, part = os.path.split(parent)
path_parts.append(part)
# Create needed directories.
for part in reversed(path_parts):
parent = os.path.join(parent, part)
try:
os.mkdir(parent)
except FileExistsError:
# Probably another Python process already created the dir.
continue
except PermissionError:
# If can't get proper access, then just forget about writing
# the data.
return
try:
with open(path, 'wb') as file:
file.write(data)
except (PermissionError, FileExistsError):
pass
def get_code(self, fullname):
return SourceLoader.get_code(self, fullname)
def get_source(self, fullname):
return SourceLoader.get_source(self, fullname)
def is_package(self, fullname):
return self.package
def load_module(self, fullname):
module = self._load_module(fullname, sourceless=True)
# Remove the module from sys list
del sys.modules[fullname]
# If the module was already loaded, then reload it
if hasattr(module, '__LOADED__'):
reload(module)
# Check that is a valid nemubot module
if not hasattr(module, "nemubotversion"):
raise ImportError("Module `%s' is not a nemubot module."%self.name)
# Check module version
if module.nemubotversion != self.context.version:
raise ImportError("Module `%s' is not compatible with this "
"version." % self.name)
# Set module common functions and datas
module.__LOADED__ = True
# Set module common functions and datas
module.DEBUG = False
module.name = fullname
module.print = lambda msg: print("[%s] %s"%(module.name, msg))
module.print_debug = lambda msg: mod_print_dbg(module, msg)
if not hasattr(module, "NODATA"):
module.DATAS = xmlparser.parse_file(self.context.datas_path
+ module.name + ".xml")
module.save = lambda: mod_save(module, self.context.datas_path)
else:
module.DATAS = None
module.save = lambda: False
module.CONF = self.config
module.has_access = lambda msg: mod_has_access(module,
module.CONF, msg)
# Load dependancies
if module.CONF is not None and module.CONF.hasNode("dependson"):
module.MODS = dict()
for depend in module.CONF.getNodes("dependson"):
for md in MODS:
if md.name == depend["name"]:
mod.MODS[md.name] = md
break
if depend["name"] not in module.MODS:
print ("\033[1;31mERROR:\033[0m in module `%s', module "
"`%s' require by this module but is not loaded."
% (module.name, depend["name"]))
return
# Add the module to the global modules list
if self.context.add_module(module):
# Launch the module
if hasattr(module, "load"):
module.load()
# Register hooks
register_hooks(module, self.context, self.prompt)
print (" Module `%s' successfully loaded." % module.name)
else:
raise ImportError("An error occurs while importing `%s'."
% module.name)
return module
def add_cap_hook(prompt, module, cmd):
if hasattr(module, cmd["call"]):
prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"]))
else:
print ("Warning: In module `%s', no function `%s' defined for `%s' "
"command hook." % (module.name, cmd["call"], cmd["name"]))
def register_hooks(module, context, prompt):
"""Register all available hooks"""
if module.CONF is not None:
# Register command hooks
if module.CONF.hasNode("command"):
for cmd in module.CONF.getNodes("command"):
if cmd.hasAttribute("name") and cmd.hasAttribute("call"):
add_cap_hook(prompt, module, cmd)
# Register message hooks
if module.CONF.hasNode("message"):
for msg in module.CONF.getNodes("message"):
context.hooks.register_hook(module, msg)
##########################
# #
# Module functions #
# #
##########################
def mod_print_dbg(mod, msg):
if mod.DEBUG:
print("{%s} %s"%(mod.name, msg))
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

View File

@ -1,18 +1,30 @@
# coding=utf-8 # -*- 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 datetime import datetime from datetime import datetime
from datetime import timedelta
import imp
import re import re
import shlex import shlex
import string
import sys
import time import time
from credits import Credits
import credits import credits
dcc = __import__("DCC") from credits import Credits
imp.reload(dcc) import DCC
import xmlparser
CREDITS = {} CREDITS = {}
filename = "" filename = ""
@ -145,13 +157,13 @@ 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, hooks):
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:
self.parsectcp () self.parsectcp ()
elif self.cmd == "PRIVMSG" and self.authorize(): elif self.cmd == "PRIVMSG" and self.authorize():
self.parsemsg (mods) self.parsemsg (hooks)
elif self.channel in self.srv.channels: elif self.channel in self.srv.channels:
if self.cmd == "353": if self.cmd == "353":
self.srv.channels[self.channel].parse353(self) self.srv.channels[self.channel].parse353(self)
@ -185,7 +197,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]))
@ -201,45 +213,26 @@ class Message:
self.srv.send_ctcp(self.sender, "ERRMSG Unknown or unimplemented CTCP request") self.srv.send_ctcp(self.sender, "ERRMSG Unknown or unimplemented CTCP request")
def reparsemsg(self): def reparsemsg(self):
if self.mods is not None: if self.hooks is not None:
self.parsemsg(self.mods) self.parsemsg(self.hooks)
else: else:
print ("Can't reparse message") print ("Can't reparse message")
def parsemsg (self, mods): def parsemsg (self, hooks):
#Treat all messages starting with 'nemubot:' as distinct commands #Treat all messages starting with 'nemubot:' as distinct commands
if self.content.find("%s:"%self.srv.nick) == 0: if self.content.find("%s:"%self.srv.nick) == 0:
#Remove the bot name #Remove the bot name
self.content = self.content[len(self.srv.nick)+1:].strip() self.content = self.content[len(self.srv.nick)+1:].strip()
messagel = self.content.lower() messagel = self.content.lower()
#Is it a simple response? # Treat ping
if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", messagel) is not None: if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
self.send_chn ("%s: pong"%(self.nick)) messagel) is not None:
self.send_chn ("%s: pong"%(self.nick))
elif re.match(".*(quel(le)? heure est[ -]il|what time is it)", messagel) is not None: # Ask hooks
now = datetime.now()
self.send_chn ("%s: j'envoie ce message à %02d:%02d:%02d."%(self.nick, now.hour, now.minute, now.second))
elif re.match(".*di[st] (a|à) ([a-zA-Z0-9_]+) (.+)$", messagel) is not None:
result = re.match(".*di[st] (a|à) ([a-zA-Z0-9_]+) (qu(e |'))?(.+)$", self.content)
self.send_chn ("%s: %s"%(result.group(2), result.group(5)))
elif re.match(".*di[st] (.+) (a|à) ([a-zA-Z0-9_]+)$", messagel) is not None:
result = re.match(".*di[st] (.+) (à|a) ([a-zA-Z0-9_]+)$", self.content)
self.send_chn ("%s: %s"%(result.group(3), result.group(1)))
elif re.match(".*di[st] sur (#[a-zA-Z0-9]+) (.+)$", self.content) is not None:
result = re.match(".*di[st] sur (#[a-zA-Z0-9]+) (.+)$", self.content)
self.send_msg(result.group(1), result.group(2))
elif re.match(".*di[st] (.+) sur (#[a-zA-Z0-9]+)$", self.content) is not None:
result = re.match(".*di[st] (.+) sur (#[a-zA-Z0-9]+)$", self.content)
self.send_msg(result.group(2), result.group(1))
#Try modules
else: else:
for im in mods: hooks.treat_ask(self)
if im.has_access(self) and im.parseask(self):
return
#Owner commands #Owner commands
elif self.content[0] == '`' and self.sender == self.srv.owner: elif self.content[0] == '`' and self.sender == self.srv.owner:
@ -264,7 +257,7 @@ class Message:
#Messages stating with ! #Messages stating with !
elif self.content[0] == '!' and len(self.content) > 1: elif self.content[0] == '!' and len(self.content) > 1:
self.mods = mods self.hooks = hooks
try: try:
self.cmd = shlex.split(self.content[1:]) self.cmd = shlex.split(self.content[1:])
except ValueError: except ValueError:
@ -297,19 +290,13 @@ class Message:
conn = dcc.DCC(self.srv, self.sender) conn = dcc.DCC(self.srv, self.sender)
conn.send_file("bot_sample.xml") conn.send_file("bot_sample.xml")
else: else:
for im in mods: hooks.treat_cmd(self)
if im.has_access(self) and im.parseanswer(self):
return
else: else:
for im in mods: hooks.treat_answer(self)
if im.has_access(self) and im.parselisten(self): # Assume the message starts with nemubot:
return if self.private:
#Assume the message starts with nemubot: hooks.treat_ask(self)
if self.private:
for im in mods:
if im.has_access(self) and im.parseask(self):
return
# def parseOwnerCmd(self, cmd): # def parseOwnerCmd(self, cmd):

View File

@ -1,42 +1,76 @@
#!/usr/bin/python3 #!/usr/bin/python3
# coding=utf-8 # -*- 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 sys import sys
import os import os
import imp import imp
import traceback import traceback
servers = dict() import bot
import prompt
from prompt.builtins import load_file
import importer
prompt = __import__ ("prompt") if __name__ == "__main__":
# Create bot context
context = bot.Bot()
#Add modules dir path # Load the prompt
if os.path.isdir("./modules/"): prmpt = prompt.Prompt()
modules_path = os.path.realpath(os.path.abspath("./modules/"))
if modules_path not in sys.path:
sys.path.insert(0, modules_path)
#Load given files # Register the hook for futur import
if len(sys.argv) >= 2: import sys
for arg in sys.argv[1:]: sys.meta_path.append(importer.ModuleFinder(context, prmpt))
if os.path.isfile(arg):
prompt.load_file(arg, servers)
elif os.path.isdir(arg):
sys.path.insert(1, arg)
print ("Nemubot ready, my PID is %i!" % (os.getpid())) #Add modules dir path
while prompt.launch(servers): if os.path.isdir("./modules/"):
try: context.add_modules_path(
if prompt.MODS is None: os.path.realpath(os.path.abspath("./modules/")))
imp.reload(prompt)
else:
mods = prompt.MODS
imp.reload(prompt)
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 ("Bye") # Parse command line arguments
sys.exit(0) if len(sys.argv) >= 2:
for arg in sys.argv[1:]:
if os.path.isdir(arg):
context.add_modules_path(arg)
else:
load_file(arg, context)
print ("Nemubot v%s ready, my PID is %i!" % (context.version_txt,
os.getpid()))
while prmpt.run(context):
try:
# Reload context
imp.reload(bot)
context = bot.hotswap(context)
# Reload prompt
imp.reload(prompt)
prmpt = prompt.hotswap(prmpt)
# Reload all other modules
bot.reload()
print ("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" %
context.version_txt)
except:
print ("\033[1;31mUnable to reload the prompt due to errors.\033[0"
"m Fix them before trying to reload the prompt.")
exc_type, exc_value, exc_traceback = sys.exc_info()
sys.stderr.write (traceback.format_exception_only(exc_type,
exc_value)[0])
print ("Bye")
sys.exit(0)

105
prompt/__init__.py Normal file
View File

@ -0,0 +1,105 @@
# -*- 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 imp
import os
import shlex
import sys
import traceback
from . import builtins
class Prompt:
def __init__(self, hc=dict(), hl=dict()):
self.selectedServer = None
self.HOOKS_CAPS = hc
self.HOOKS_LIST = hl
def add_cap_hook(self, name, call, data=None):
self.HOOKS_CAPS[name] = (lambda d, t, c, p: call(d, t, c, p), data)
def lex_cmd(self, line):
"""Return an array of tokens"""
ret = list()
try:
cmds = shlex.split(line)
bgn = 0
for i in range(0, len(cmds)):
if cmds[i] == ';':
if i != bgn:
cmds[bgn] = cmds[bgn].lower()
ret.append(cmds[bgn:i])
bgn = i + 1
if bgn != len(cmds):
cmds[bgn] = cmds[bgn].lower()
ret.append(cmds[bgn:len(cmds)])
return ret
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
sys.stderr.write (traceback.format_exception_only(
exc_type, exc_value)[0])
return ret
def exec_cmd(self, toks, context):
"""Execute the command"""
if toks[0] in builtins.CAPS:
return builtins.CAPS[toks[0]](toks, context, self)
elif toks[0] in self.HOOKS_CAPS:
(f,d) = self.HOOKS_CAPS[toks[0]]
return f(d, toks, context, self)
else:
print ("Unknown command: `%s'" % toks[0])
return ""
def getPS1(self):
"""Get the PS1 associated to the selected server"""
if self.selectedServer is None:
return "nemubot"
else:
return self.selectedServer.id
def run(self, context):
"""Launch the prompt"""
ret = ""
while ret != "quit" and ret != "reset" and ret != "refresh":
sys.stdout.write("\033[0;33m%s§\033[0m " % self.getPS1())
sys.stdout.flush()
try:
line = sys.stdin.readline()
if len(line) <= 0:
line = "quit"
print ("quit")
cmds = self.lex_cmd(line.strip())
for toks in cmds:
try:
ret = self.exec_cmd(toks, context)
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value, exc_traceback)
except KeyboardInterrupt:
print ("")
return ret != "quit"
def hotswap(prompt):
return Prompt(prompt.HOOKS_CAPS, prompt.HOOKS_LIST)

140
prompt/builtins.py Normal file
View File

@ -0,0 +1,140 @@
# -*- 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 os
import xmlparser
def end(toks, context, prompt):
"""Quit the prompt for reload or exit"""
if toks[0] == "refresh":
return "refresh"
elif toks[0] == "reset":
return "reset"
else:
context.quit()
return "quit"
def liste(toks, context, prompt):
"""Show some lists"""
if len(toks) > 1:
for l in toks[1:]:
l = l.lower()
if l == "server" or l == "servers":
for srv in context.servers.keys():
print (" - %s ;" % srv)
else:
print (" > No server loaded")
elif l == "mod" or l == "mods" or l == "module" or l == "modules":
for mod in context.modules.keys():
print (" - %s ;" % mod)
else:
print (" > No module loaded")
elif l in prompt.HOOKS_LIST:
(f,d) = prompt.HOOKS_LIST[l]
f(d, context, prompt)
else:
print (" Unknown list `%s'" % l)
else:
print (" Please give a list to show: servers, ...")
def load_file(filename, context):
if os.path.isfile(filename):
config = xmlparser.parse_file(filename)
# This is a true nemubot configuration file, load it!
if (config.getName() == "nemubotconfig"
or config.getName() == "config"):
# Preset each server in this file
for server in config.getNodes("server"):
if context.addServer(server, config["nick"],
config["owner"], config["realname"]):
print (" Server `%s:%s' successfully added."
% (server["server"], server["port"]))
else:
print (" Server `%s:%s' already added, skiped."
% (server["server"], server["port"]))
# Load files asked by the configuration file
for load in config.getNodes("load"):
load_file(load["path"], context)
# This is a nemubot module configuration file, load the module
elif config.getName() == "nemubotmodule":
__import__(config["name"])
# 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:
__import__(filename)
def load(toks, context, prompt):
"""Load an XML configuration file"""
if len(toks) > 1:
for filename in toks[1:]:
load_file(filename, context)
else:
print ("Not enough arguments. `load' takes a filename.")
return
def select(toks, context, prompt):
"""Select the current server"""
if (len(toks) == 2 and toks[1] != "None"
and toks[1] != "nemubot" and toks[1] != "none"):
if toks[1] in context.servers:
prompt.selectedServer = context.servers[toks[1]]
else:
print ("select: server `%s' not found." % toks[1])
else:
prompt.selectedServer = None
return
def unload(toks, context, prompt):
"""Unload a module"""
if len(toks) == 2 and toks[1] == "all":
for name in context.modules.keys():
context.unload_module(name)
elif len(toks) > 1:
for name in toks[1:]:
if context.unload_module(name):
print (" Module `%s' successfully unloaded." % name)
else:
print (" No module `%s' loaded, can't unload!" % name)
else:
print ("Not enough arguments. `unload' takes a module name.")
#Register build-ins
CAPS = {
'quit': end, #Disconnect all server and quit
'exit': end, #Alias for quit
'reset': end, #Reload the prompt
'refresh': end, #Reload the prompt but save modules
'load': load, #Load a servers or module configuration file
'unload': unload, #Unload a module and remove it from the list
'select': select, #Select a server
'list': liste, #Show lists
}

View File

@ -1,16 +1,30 @@
import imp # -*- 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 socket
import sys import sys
import threading import threading
import traceback import traceback
import time
message = __import__("message") import channel
imp.reload(message) import DCC
channel = __import__("channel") import message
imp.reload(channel) import xmlparser
dcc = __import__("DCC")
imp.reload(dcc)
class Server(threading.Thread): class Server(threading.Thread):
def __init__(self, node, nick, owner, realname, socket = None): def __init__(self, node, nick, owner, realname, socket = None):
@ -216,22 +230,19 @@ class Server(threading.Thread):
else: else:
return False return False
def update_mods(self, mods): def launch(self, context, verb=True):
self.mods = mods
def launch(self, mods):
"""Connect to the server if it is no yet connected""" """Connect to the server if it is no yet connected"""
self.context = context
if not self.connected: if not self.connected:
self.stop = False self.stop = False
self.mods = mods
self.start() self.start()
else: elif verb:
print (" Already connected.") print (" Already connected.")
def treat_msg(self, line, private = False): def treat_msg(self, line, private = False):
try: try:
msg = message.Message (self, line, private) msg = message.Message (self, line, private)
msg.treat (self.mods) msg.treat(self.context.hooks)
except: except:
print ("\033[1;31mERROR:\033[0m occurred during the processing of the message: %s" % line) print ("\033[1;31mERROR:\033[0m occurred during the processing of the message: %s" % line)
exc_type, exc_value, exc_traceback = sys.exc_info() exc_type, exc_value, exc_traceback = sys.exc_info()

View File

@ -1,12 +1,26 @@
#!/usr/bin/python3 # -*- coding: utf-8 -*-
# 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 os import os
import imp import imp
import xml.sax import xml.sax
module_state = __import__("module_state") from . import node as module_state
imp.reload(module_state)
class ModuleStatesFile(xml.sax.ContentHandler): class ModuleStatesFile(xml.sax.ContentHandler):
def startDocument(self): def startDocument(self):