From e4d4e68c4546191cb2eaace230c11fe8a930dfc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9munaire?= Date: Tue, 14 Aug 2012 05:51:55 +0200 Subject: [PATCH] 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) --- COPYING | 141 +++++------ bot.py | 129 ++++++++++ hooks.py | 151 ++++++++++++ importer.py | 229 ++++++++++++++++++ message.py | 89 +++---- nemubot.py | 94 ++++--- prompt/__init__.py | 105 ++++++++ prompt/builtins.py | 140 +++++++++++ server.py | 41 ++-- .../__init__.py | 22 +- module_state.py => xmlparser/node.py | 0 11 files changed, 964 insertions(+), 177 deletions(-) create mode 100644 bot.py create mode 100644 hooks.py create mode 100644 importer.py create mode 100644 prompt/__init__.py create mode 100644 prompt/builtins.py rename module_states_file.py => xmlparser/__init__.py (61%) rename module_state.py => xmlparser/node.py (100%) diff --git a/COPYING b/COPYING index 94a9ed0..dba13ed 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +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 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 -software for all its users. We, the Free Software Foundation, use the -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. +software for all its users. When we speak of free software, we are referring to freedom, not 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 free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +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: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +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 -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - 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. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 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 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 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 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 License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. 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 -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 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 option of following the terms and conditions either of that numbered version or of any later version published by the Free Software 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. 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 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) 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 (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 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 . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -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". + If your software can interact with users remotely through a computer +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 +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. 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. -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 . - - 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 -. diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..e4bf999 --- /dev/null +++ b/bot.py @@ -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 . + +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) diff --git a/hooks.py b/hooks.py new file mode 100644 index 0000000..0bc6d0e --- /dev/null +++ b/hooks.py @@ -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 . + +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) diff --git a/importer.py b/importer.py new file mode 100644 index 0000000..c745827 --- /dev/null +++ b/importer.py @@ -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 . + +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 diff --git a/message.py b/message.py index 55f6f86..94491c6 100644 --- a/message.py +++ b/message.py @@ -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 . from datetime import datetime -from datetime import timedelta -import imp import re import shlex -import string -import sys import time -from credits import Credits import credits -dcc = __import__("DCC") -imp.reload(dcc) +from credits import Credits +import DCC +import xmlparser CREDITS = {} filename = "" @@ -145,13 +157,13 @@ class Message: return False return self.srv.accepted_channel(self.channel) - def treat(self, mods): + def treat(self, hooks): if self.cmd == "PING": self.pong () elif self.cmd == "PRIVMSG" and self.ctcp: self.parsectcp () elif self.cmd == "PRIVMSG" and self.authorize(): - self.parsemsg (mods) + self.parsemsg (hooks) elif self.channel in self.srv.channels: if self.cmd == "353": self.srv.channels[self.channel].parse353(self) @@ -185,7 +197,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])) @@ -201,45 +213,26 @@ class Message: self.srv.send_ctcp(self.sender, "ERRMSG Unknown or unimplemented CTCP request") def reparsemsg(self): - if self.mods is not None: - self.parsemsg(self.mods) + if self.hooks is not None: + self.parsemsg(self.hooks) else: print ("Can't reparse message") - def parsemsg (self, mods): + def parsemsg (self, hooks): #Treat all messages starting with 'nemubot:' as distinct commands if self.content.find("%s:"%self.srv.nick) == 0: #Remove the bot name self.content = self.content[len(self.srv.nick)+1:].strip() messagel = self.content.lower() - #Is it a simple response? - if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", messagel) is not None: - self.send_chn ("%s: pong"%(self.nick)) + # Treat ping + if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", + 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: - 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 + # Ask hooks else: - for im in mods: - if im.has_access(self) and im.parseask(self): - return + hooks.treat_ask(self) #Owner commands elif self.content[0] == '`' and self.sender == self.srv.owner: @@ -264,7 +257,7 @@ class Message: #Messages stating with ! elif self.content[0] == '!' and len(self.content) > 1: - self.mods = mods + self.hooks = hooks try: self.cmd = shlex.split(self.content[1:]) except ValueError: @@ -297,19 +290,13 @@ class Message: conn = dcc.DCC(self.srv, self.sender) conn.send_file("bot_sample.xml") else: - for im in mods: - if im.has_access(self) and im.parseanswer(self): - return + hooks.treat_cmd(self) else: - for im in mods: - if im.has_access(self) and im.parselisten(self): - return - #Assume the message starts with nemubot: - if self.private: - for im in mods: - if im.has_access(self) and im.parseask(self): - return + hooks.treat_answer(self) + # Assume the message starts with nemubot: + if self.private: + hooks.treat_ask(self) # def parseOwnerCmd(self, cmd): diff --git a/nemubot.py b/nemubot.py index 3c54070..4f07b6b 100755 --- a/nemubot.py +++ b/nemubot.py @@ -1,42 +1,76 @@ #!/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 . import sys import os import imp 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 -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) + # Load the prompt + prmpt = prompt.Prompt() -#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) + # Register the hook for futur import + import sys + sys.meta_path.append(importer.ModuleFinder(context, prmpt)) -print ("Nemubot ready, my PID is %i!" % (os.getpid())) -while prompt.launch(servers): - try: - if prompt.MODS is None: - 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]) + #Add modules dir path + if os.path.isdir("./modules/"): + context.add_modules_path( + os.path.realpath(os.path.abspath("./modules/"))) -print ("Bye") -sys.exit(0) + # Parse command line arguments + 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) diff --git a/prompt/__init__.py b/prompt/__init__.py new file mode 100644 index 0000000..62c8dc3 --- /dev/null +++ b/prompt/__init__.py @@ -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 . + +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) diff --git a/prompt/builtins.py b/prompt/builtins.py new file mode 100644 index 0000000..d569f40 --- /dev/null +++ b/prompt/builtins.py @@ -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 . + +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 +} diff --git a/server.py b/server.py index 3c92182..228a55b 100644 --- a/server.py +++ b/server.py @@ -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 . + import socket import sys import threading import traceback -import time -message = __import__("message") -imp.reload(message) -channel = __import__("channel") -imp.reload(channel) -dcc = __import__("DCC") -imp.reload(dcc) +import channel +import DCC +import message +import xmlparser class Server(threading.Thread): def __init__(self, node, nick, owner, realname, socket = None): @@ -216,22 +230,19 @@ class Server(threading.Thread): else: return False - def update_mods(self, mods): - self.mods = mods - - def launch(self, mods): + def launch(self, context, verb=True): """Connect to the server if it is no yet connected""" + self.context = context if not self.connected: self.stop = False - self.mods = mods self.start() - else: + elif verb: print (" Already connected.") def treat_msg(self, line, private = False): try: msg = message.Message (self, line, private) - msg.treat (self.mods) + msg.treat(self.context.hooks) 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() diff --git a/module_states_file.py b/xmlparser/__init__.py similarity index 61% rename from module_states_file.py rename to xmlparser/__init__.py index 3da3352..adfb85b 100644 --- a/module_states_file.py +++ b/xmlparser/__init__.py @@ -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 . import os import imp import xml.sax -module_state = __import__("module_state") -imp.reload(module_state) +from . import node as module_state class ModuleStatesFile(xml.sax.ContentHandler): def startDocument(self): diff --git a/module_state.py b/xmlparser/node.py similarity index 100% rename from module_state.py rename to xmlparser/node.py