| @@ -1,9 +1,3 @@ | |||
| # Ignore bot-specific files: | |||
| logs/ | |||
| config.yml | |||
| sites.db | |||
| .cookies | |||
| # Ignore python bytecode: | |||
| *.pyc | |||
| @@ -1,70 +0,0 @@ | |||
| #! /usr/bin/env python | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot | |||
| This is a thin wrapper for EarwigBot's main bot code, specified by bot_script. | |||
| The wrapper will automatically restart the bot when it shuts down (from | |||
| !restart, for example). It requests the bot's password at startup and reuses it | |||
| every time the bot restarts internally, so you do not need to re-enter the | |||
| password after using !restart. | |||
| For information about the bot as a whole, see the attached README.md file (in | |||
| markdown format!), the docs/ directory, and the LICENSE file for licensing | |||
| information. EarwigBot is released under the MIT license. | |||
| """ | |||
| from getpass import getpass | |||
| from subprocess import Popen, PIPE | |||
| from os import path | |||
| from sys import executable | |||
| from time import sleep | |||
| import earwigbot | |||
| bot_script = path.join(earwigbot.__path__[0], "runner.py") | |||
| def main(): | |||
| print "EarwigBot v{0}\n".format(earwigbot.__version__) | |||
| is_encrypted = earwigbot.config.config.load() | |||
| if is_encrypted: # Passwords in the config file are encrypted | |||
| key = getpass("Enter key to unencrypt bot passwords: ") | |||
| else: | |||
| key = None | |||
| while 1: | |||
| bot = Popen([executable, bot_script], stdin=PIPE) | |||
| print >> bot.stdin, path.dirname(path.abspath(__file__)) | |||
| if is_encrypted: | |||
| print >> bot.stdin, key | |||
| return_code = bot.wait() | |||
| if return_code == 1: | |||
| exit() # Let critical exceptions in the subprocess cause us to | |||
| # exit as well | |||
| else: | |||
| sleep(5) # Sleep between bot runs following a non-critical | |||
| # subprocess exit | |||
| if __name__ == "__main__": | |||
| main() | |||
| @@ -31,6 +31,4 @@ __license__ = "MIT License" | |||
| __version__ = "0.1.dev" | |||
| __email__ = "ben.kurtovic@verizon.net" | |||
| from earwigbot import ( | |||
| blowfish, commands, config, irc, main, runner, tasks, tests, wiki | |||
| ) | |||
| from earwigbot import blowfish, bot, commands, config, irc, tasks, util, wiki | |||
| @@ -0,0 +1,113 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| import threading | |||
| from time import sleep, time | |||
| from earwigbot.config import BotConfig | |||
| from earwigbot.irc import Frontend, Watcher | |||
| from earwigbot.tasks import task_manager | |||
| class Bot(object): | |||
| """ | |||
| The Bot class is the core of EarwigBot, essentially responsible for | |||
| starting the various bot components and making sure they are all happy. An | |||
| explanation of the different components follows: | |||
| EarwigBot has three components that can run independently of each other: an | |||
| IRC front-end, an IRC watcher, and a wiki scheduler. | |||
| * The IRC front-end runs on a normal IRC server and expects users to | |||
| interact with it/give it commands. | |||
| * The IRC watcher runs on a wiki recent-changes server and listens for | |||
| edits. Users cannot interact with this part of the bot. | |||
| * The wiki scheduler runs wiki-editing bot tasks in separate threads at | |||
| user-defined times through a cron-like interface. | |||
| """ | |||
| def __init__(self, root_dir): | |||
| self.config = BotConfig(root_dir) | |||
| self.logger = logging.getLogger("earwigbot") | |||
| self.frontend = None | |||
| self.watcher = None | |||
| self._keep_scheduling = True | |||
| self._lock = threading.Lock() | |||
| def _start_thread(self, name, target): | |||
| thread = threading.Thread(name=name, target=target) | |||
| thread.start() | |||
| def _wiki_scheduler(self): | |||
| while self._keep_scheduling: | |||
| time_start = time() | |||
| task_manager.schedule() | |||
| time_end = time() | |||
| time_diff = time_start - time_end | |||
| if time_diff < 60: # Sleep until the next minute | |||
| sleep(60 - time_diff) | |||
| def _start_components(self): | |||
| if self.config.components.get("irc_frontend"): | |||
| self.logger.info("Starting IRC frontend") | |||
| self.frontend = Frontend(self.config) | |||
| self._start_thread(name, self.frontend.loop) | |||
| if self.config.components.get("irc_watcher"): | |||
| self.logger.info("Starting IRC watcher") | |||
| self.watcher = Watcher(self.config, self.frontend) | |||
| self._start_thread(name, self.watcher.loop) | |||
| if self.config.components.get("wiki_scheduler"): | |||
| self.logger.info("Starting wiki scheduler") | |||
| self._start_thread(name, self._wiki_scheduler) | |||
| def _loop(self): | |||
| while 1: | |||
| with self._lock: | |||
| if self.frontend and self.frontend.is_stopped(): | |||
| self.frontend._connect() | |||
| if self.watcher and self.watcher.is_stopped(): | |||
| self.watcher._connect() | |||
| sleep(5) | |||
| def run(self): | |||
| self.config.load() | |||
| self.config.decrypt(config.wiki, "password") | |||
| self.config.decrypt(config.wiki, "search", "credentials", "key") | |||
| self.config.decrypt(config.wiki, "search", "credentials", "secret") | |||
| self.config.decrypt(config.irc, "frontend", "nickservPassword") | |||
| self.config.decrypt(config.irc, "watcher", "nickservPassword") | |||
| self._start_components() | |||
| self._loop() | |||
| def reload(self): | |||
| #components = self.config.components | |||
| with self._lock: | |||
| self.config.load() | |||
| #if self.config.components.get("irc_frontend"): | |||
| def stop(self): | |||
| if self.frontend: | |||
| self.frontend.stop() | |||
| if self.watcher: | |||
| self.watcher.stop() | |||
| self._keep_scheduling = False | |||
| @@ -34,4 +34,4 @@ class Command(BaseCommand): | |||
| return | |||
| self.connection.logger.info("Restarting bot per owner request") | |||
| self.connection.is_running = False | |||
| self.connection.stop() | |||
| @@ -78,10 +78,9 @@ class Command(BaseCommand): | |||
| for thread in threads: | |||
| tname = thread.name | |||
| if tname == "MainThread": | |||
| tname = self.get_main_thread_name() | |||
| t = "\x0302{0}\x0301 (as main thread, id {1})" | |||
| normal_threads.append(t.format(tname, thread.ident)) | |||
| elif tname in ["irc-frontend", "irc-watcher", "wiki-scheduler"]: | |||
| t = "\x0302MainThread\x0301 (id {1})" | |||
| normal_threads.append(t.format(thread.ident)) | |||
| elif tname in config.components: | |||
| t = "\x0302{0}\x0301 (id {1})" | |||
| normal_threads.append(t.format(tname, thread.ident)) | |||
| elif tname.startswith("reminder"): | |||
| @@ -157,12 +156,3 @@ class Command(BaseCommand): | |||
| task_manager.start(task_name, **data.kwargs) | |||
| msg = "task \x0302{0}\x0301 started.".format(task_name) | |||
| self.connection.reply(data, msg) | |||
| def get_main_thread_name(self): | |||
| """Return the "proper" name of the MainThread.""" | |||
| if "irc_frontend" in config.components: | |||
| return "irc-frontend" | |||
| elif "wiki_schedule" in config.components: | |||
| return "wiki-scheduler" | |||
| else: | |||
| return "irc-watcher" | |||
| @@ -20,31 +20,7 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot's YAML Config File Parser | |||
| This handles all tasks involving reading and writing to our config file, | |||
| including encrypting and decrypting passwords and making a new config file from | |||
| scratch at the inital bot run. | |||
| Usually you'll just want to do "from earwigbot.config import config", which | |||
| returns a singleton _BotConfig object, with data accessible from various | |||
| attributes and functions: | |||
| * config.components - enabled components | |||
| * config.wiki - information about wiki-editing | |||
| * config.tasks - information for bot tasks | |||
| * config.irc - information about IRC | |||
| * config.metadata - miscellaneous information | |||
| * config.schedule() - tasks scheduled to run at a given time | |||
| Additionally, _BotConfig has some functions used in config loading: | |||
| * config.load() - loads and parses our config file, returning True if | |||
| passwords are stored encrypted or False otherwise | |||
| * config.decrypt() - given a key, decrypts passwords inside our config | |||
| variables; won't work if passwords aren't encrypted | |||
| """ | |||
| from getpass import getpass | |||
| import logging | |||
| import logging.handlers | |||
| from os import mkdir, path | |||
| @@ -53,44 +29,36 @@ import yaml | |||
| from earwigbot import blowfish | |||
| __all__ = ["config"] | |||
| class _ConfigNode(object): | |||
| def __iter__(self): | |||
| for key in self.__dict__.iterkeys(): | |||
| yield key | |||
| def __getitem__(self, item): | |||
| return self.__dict__.__getitem__(item) | |||
| def _dump(self): | |||
| data = self.__dict__.copy() | |||
| for key, val in data.iteritems(): | |||
| if isinstance(val, _ConfigNode): | |||
| data[key] = val._dump() | |||
| return data | |||
| def _load(self, data): | |||
| self.__dict__ = data.copy() | |||
| def _decrypt(self, key, intermediates, item): | |||
| base = self.__dict__ | |||
| try: | |||
| for inter in intermediates: | |||
| base = base[inter] | |||
| except KeyError: | |||
| return | |||
| if item in base: | |||
| base[item] = blowfish.decrypt(key, base[item]) | |||
| def get(self, *args, **kwargs): | |||
| return self.__dict__.get(*args, **kwargs) | |||
| class _BotConfig(object): | |||
| def __init__(self): | |||
| self._script_dir = path.dirname(path.abspath(__file__)) | |||
| self._root_dir = path.split(self._script_dir)[0] | |||
| class BotConfig(object): | |||
| """ | |||
| EarwigBot's YAML Config File Manager | |||
| This handles all tasks involving reading and writing to our config file, | |||
| including encrypting and decrypting passwords and making a new config file | |||
| from scratch at the inital bot run. | |||
| BotConfig has a few properties and functions, including the following: | |||
| * config.root_dir - bot's working directory; contains config.yml, logs/ | |||
| * config.path - path to the bot's config file | |||
| * config.components - enabled components | |||
| * config.wiki - information about wiki-editing | |||
| * config.tasks - information for bot tasks | |||
| * config.irc - information about IRC | |||
| * config.metadata - miscellaneous information | |||
| * config.schedule() - tasks scheduled to run at a given time | |||
| BotConfig also has some functions used in config loading: | |||
| * config.load() - loads and parses our config file, returning True if | |||
| passwords are stored encrypted or False otherwise; | |||
| can also be used to easily reload config | |||
| * config.decrypt() - given a key, decrypts passwords inside our config | |||
| variables, and remembers to decrypt the password if | |||
| config is reloaded; won't do anything if passwords | |||
| aren't encrypted | |||
| """ | |||
| def __init__(self, root_dir): | |||
| self._root_dir = root_dir | |||
| self._config_path = path.join(self._root_dir, "config.yml") | |||
| self._log_dir = path.join(self._root_dir, "logs") | |||
| self._decryption_key = None | |||
| @@ -104,17 +72,17 @@ class _BotConfig(object): | |||
| self._nodes = [self._components, self._wiki, self._tasks, self._irc, | |||
| self._metadata] | |||
| self._decryptable_nodes = [] | |||
| def _load(self): | |||
| """Load data from our JSON config file (config.yml) into _config.""" | |||
| """Load data from our JSON config file (config.yml) into self._data.""" | |||
| filename = self._config_path | |||
| with open(filename, 'r') as fp: | |||
| try: | |||
| self._data = yaml.load(fp) | |||
| except yaml.YAMLError as error: | |||
| print "Error parsing config file {0}:".format(filename) | |||
| print error | |||
| exit(1) | |||
| raise | |||
| def _setup_logging(self): | |||
| """Configures the logging module so it works the way we want it to.""" | |||
| @@ -135,7 +103,7 @@ class _BotConfig(object): | |||
| else: | |||
| msg = "log_dir ({0}) exists but is not a directory!" | |||
| print msg.format(log_dir) | |||
| exit(1) | |||
| return | |||
| main_handler = hand(logfile("bot.log"), "midnight", 1, 7) | |||
| error_handler = hand(logfile("error.log"), "W6", 1, 4) | |||
| @@ -149,27 +117,29 @@ class _BotConfig(object): | |||
| h.setFormatter(formatter) | |||
| logger.addHandler(h) | |||
| stream_handler = logging.StreamHandler() | |||
| stream_handler.setLevel(logging.DEBUG) | |||
| stream_handler.setFormatter(color_formatter) | |||
| logger.addHandler(stream_handler) | |||
| stream_handler = logging.StreamHandler() | |||
| stream_handler.setLevel(logging.DEBUG) | |||
| stream_handler.setFormatter(color_formatter) | |||
| logger.addHandler(stream_handler) | |||
| else: | |||
| logger.addHandler(logging.NullHandler()) | |||
| def _decrypt(self, node, nodes): | |||
| """Try to decrypt the contents of a config node. Use self.decrypt().""" | |||
| try: | |||
| node._decrypt(self._decryption_key, nodes[:-1], nodes[-1]) | |||
| except blowfish.BlowfishError as error: | |||
| print "Error decrypting passwords:" | |||
| raise | |||
| def _make_new(self): | |||
| """Make a new config file based on the user's input.""" | |||
| encrypt = raw_input("Would you like to encrypt passwords stored in config.yml? [y/n] ") | |||
| if encrypt.lower().startswith("y"): | |||
| is_encrypted = True | |||
| else: | |||
| is_encrypted = False | |||
| return is_encrypted | |||
| @property | |||
| def script_dir(self): | |||
| return self._script_dir | |||
| #m = "Would you like to encrypt passwords stored in config.yml? [y/n] " | |||
| #encrypt = raw_input(m) | |||
| #if encrypt.lower().startswith("y"): | |||
| # is_encrypted = True | |||
| #else: | |||
| # is_encrypted = False | |||
| raise NotImplementedError() | |||
| # yaml.dumps() | |||
| @property | |||
| def root_dir(self): | |||
| @@ -182,7 +152,7 @@ class _BotConfig(object): | |||
| @property | |||
| def log_dir(self): | |||
| return self._log_dir | |||
| @property | |||
| def data(self): | |||
| """The entire config file.""" | |||
| @@ -221,7 +191,7 @@ class _BotConfig(object): | |||
| """Return True if passwords are encrypted, otherwise False.""" | |||
| return self.metadata.get("encryptPasswords", False) | |||
| def load(self, config_path=None, log_dir=None): | |||
| def load(self): | |||
| """Load, or reload, our config file. | |||
| First, check if we have a valid config file, and if not, notify the | |||
| @@ -232,19 +202,14 @@ class _BotConfig(object): | |||
| wiki, tasks, irc, metadata) for easy access (as well as the internal | |||
| _data variable). | |||
| If everything goes well, return True if stored passwords are | |||
| encrypted in the file, or False if they are not. | |||
| If config is being reloaded, encrypted items will be automatically | |||
| decrypted if they were decrypted beforehand. | |||
| """ | |||
| if config_path: | |||
| self._config_path = config_path | |||
| if log_dir: | |||
| self._log_dir = log_dir | |||
| if not path.exists(self._config_path): | |||
| print "You haven't configured the bot yet!" | |||
| choice = raw_input("Would you like to do this now? [y/n] ") | |||
| if choice.lower().startswith("y"): | |||
| return self._make_new() | |||
| self._make_new() | |||
| else: | |||
| exit(1) | |||
| @@ -257,25 +222,28 @@ class _BotConfig(object): | |||
| self.metadata._load(data.get("metadata", {})) | |||
| self._setup_logging() | |||
| return self.is_encrypted() | |||
| if self.is_encrypted(): | |||
| if not self._decryption_key: | |||
| key = getpass("Enter key to decrypt bot passwords: ") | |||
| self._decryption_key = key | |||
| for node, nodes in self._decryptable_nodes: | |||
| self._decrypt(node, nodes) | |||
| def decrypt(self, node, *nodes): | |||
| """Use self._decryption_key to decrypt an object in our config tree. | |||
| If this is called when passwords are not encrypted (check with | |||
| config.is_encrypted()), nothing will happen. | |||
| config.is_encrypted()), nothing will happen. We'll also keep track of | |||
| this node if config.load() is called again (i.e. to reload) and | |||
| automatically decrypt it. | |||
| An example usage would be: | |||
| Example usage: | |||
| config.decrypt(config.irc, "frontend", "nickservPassword") | |||
| -> decrypts config.irc["frontend"]["nickservPassword"] | |||
| """ | |||
| if not self.is_encrypted(): | |||
| return | |||
| try: | |||
| node._decrypt(self._decryption_key, nodes[:-1], nodes[-1]) | |||
| except blowfish.BlowfishError as error: | |||
| print "\nError decrypting passwords:" | |||
| print "{0}: {1}.".format(error.__class__.__name__, error) | |||
| exit(1) | |||
| self._decryptable_nodes.append((node, nodes)) | |||
| if self.is_encrypted(): | |||
| self._decrypt(node, nodes) | |||
| def schedule(self, minute, hour, month_day, month, week_day): | |||
| """Return a list of tasks scheduled to run at the specified time. | |||
| @@ -311,6 +279,38 @@ class _BotConfig(object): | |||
| return tasks | |||
| class _ConfigNode(object): | |||
| def __iter__(self): | |||
| for key in self.__dict__.iterkeys(): | |||
| yield key | |||
| def __getitem__(self, item): | |||
| return self.__dict__.__getitem__(item) | |||
| def _dump(self): | |||
| data = self.__dict__.copy() | |||
| for key, val in data.iteritems(): | |||
| if isinstance(val, _ConfigNode): | |||
| data[key] = val._dump() | |||
| return data | |||
| def _load(self, data): | |||
| self.__dict__ = data.copy() | |||
| def _decrypt(self, key, intermediates, item): | |||
| base = self.__dict__ | |||
| try: | |||
| for inter in intermediates: | |||
| base = base[inter] | |||
| except KeyError: | |||
| return | |||
| if item in base: | |||
| base[item] = blowfish.decrypt(key, base[item]) | |||
| def get(self, *args, **kwargs): | |||
| return self.__dict__.get(*args, **kwargs) | |||
| class _BotFormatter(logging.Formatter): | |||
| def __init__(self, color=False): | |||
| self._format = super(_BotFormatter, self).format | |||
| @@ -336,6 +336,3 @@ class _BotFormatter(logging.Formatter): | |||
| if record.levelno == logging.CRITICAL: | |||
| record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red | |||
| return record | |||
| config = _BotConfig() | |||
| @@ -22,6 +22,7 @@ | |||
| import socket | |||
| import threading | |||
| from time import sleep | |||
| __all__ = ["BrokenSocketException", "IRCConnection"] | |||
| @@ -42,7 +43,7 @@ class IRCConnection(object): | |||
| self.ident = ident | |||
| self.realname = realname | |||
| self.logger = logger | |||
| self.is_running = False | |||
| self._is_running = False | |||
| # A lock to prevent us from sending two messages at once: | |||
| self._lock = threading.Lock() | |||
| @@ -53,8 +54,9 @@ class IRCConnection(object): | |||
| try: | |||
| self._sock.connect((self.host, self.port)) | |||
| except socket.error: | |||
| self.logger.critical("Couldn't connect to IRC server", exc_info=1) | |||
| exit(1) | |||
| self.logger.exception("Couldn't connect to IRC server") | |||
| sleep(8) | |||
| self._connect() | |||
| self._send("NICK {0}".format(self.nick)) | |||
| self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname)) | |||
| @@ -68,7 +70,7 @@ class IRCConnection(object): | |||
| def _get(self, size=4096): | |||
| """Receive (i.e. get) data from the server.""" | |||
| data = self._sock.recv(4096) | |||
| data = self._sock.recv(size) | |||
| if not data: | |||
| # Socket isn't giving us any data, so it is dead or broken: | |||
| raise BrokenSocketException() | |||
| @@ -121,21 +123,38 @@ class IRCConnection(object): | |||
| msg = "PONG {0}".format(target) | |||
| self._send(msg) | |||
| def quit(self, msg=None): | |||
| """Issue a quit message to the server.""" | |||
| if msg: | |||
| self._send("QUIT {0}".format(msg)) | |||
| else: | |||
| self._send("QUIT") | |||
| def loop(self): | |||
| """Main loop for the IRC connection.""" | |||
| self.is_running = True | |||
| self._is_running = True | |||
| read_buffer = "" | |||
| while 1: | |||
| try: | |||
| read_buffer += self._get() | |||
| except BrokenSocketException: | |||
| self.is_running = False | |||
| self._is_running = False | |||
| break | |||
| lines = read_buffer.split("\n") | |||
| read_buffer = lines.pop() | |||
| for line in lines: | |||
| self._process_message(line) | |||
| if not self.is_running: | |||
| if self.is_stopped(): | |||
| self._close() | |||
| break | |||
| def stop(self): | |||
| """Request the IRC connection to close at earliest convenience.""" | |||
| if self._is_running: | |||
| self.quit() | |||
| self._is_running = False | |||
| def is_stopped(self): | |||
| """Return whether the IRC connection has been (or is to be) closed.""" | |||
| return not self._is_running | |||
| @@ -25,7 +25,6 @@ import re | |||
| from earwigbot.commands import command_manager | |||
| from earwigbot.irc import IRCConnection, Data, BrokenSocketException | |||
| from earwigbot.config import config | |||
| __all__ = ["Frontend"] | |||
| @@ -41,7 +40,8 @@ class Frontend(IRCConnection): | |||
| """ | |||
| sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | |||
| def __init__(self): | |||
| def __init__(self, config): | |||
| self.config = config | |||
| self.logger = logging.getLogger("earwigbot.frontend") | |||
| cf = config.irc["frontend"] | |||
| base = super(Frontend, self) | |||
| @@ -66,7 +66,7 @@ class Frontend(IRCConnection): | |||
| data.msg = " ".join(line[3:])[1:] | |||
| data.chan = line[2] | |||
| if data.chan == config.irc["frontend"]["nick"]: | |||
| if data.chan == self.config.irc["frontend"]["nick"]: | |||
| # This is a privmsg to us, so set 'chan' as the nick of the | |||
| # sender, then check for private-only command hooks: | |||
| data.chan = data.nick | |||
| @@ -86,8 +86,8 @@ class Frontend(IRCConnection): | |||
| elif line[1] == "376": | |||
| # If we're supposed to auth to NickServ, do that: | |||
| try: | |||
| username = config.irc["frontend"]["nickservUsername"] | |||
| password = config.irc["frontend"]["nickservPassword"] | |||
| username = self.config.irc["frontend"]["nickservUsername"] | |||
| password = self.config.irc["frontend"]["nickservPassword"] | |||
| except KeyError: | |||
| pass | |||
| else: | |||
| @@ -95,5 +95,5 @@ class Frontend(IRCConnection): | |||
| self.say("NickServ", msg) | |||
| # Join all of our startup channels: | |||
| for chan in config.irc["frontend"]["channels"]: | |||
| for chan in self.config.irc["frontend"]["channels"]: | |||
| self.join(chan) | |||
| @@ -24,7 +24,6 @@ import imp | |||
| import logging | |||
| from earwigbot.irc import IRCConnection, RC, BrokenSocketException | |||
| from earwigbot.config import config | |||
| __all__ = ["Watcher"] | |||
| @@ -39,7 +38,8 @@ class Watcher(IRCConnection): | |||
| to channels on the IRC frontend. | |||
| """ | |||
| def __init__(self, frontend=None): | |||
| def __init__(self, config, frontend=None): | |||
| self.config = config | |||
| self.logger = logging.getLogger("earwigbot.watcher") | |||
| cf = config.irc["watcher"] | |||
| base = super(Watcher, self) | |||
| @@ -58,7 +58,7 @@ class Watcher(IRCConnection): | |||
| # Ignore messages originating from channels not in our list, to | |||
| # prevent someone PMing us false data: | |||
| if chan not in config.irc["watcher"]["channels"]: | |||
| if chan not in self.config.irc["watcher"]["channels"]: | |||
| return | |||
| msg = " ".join(line[3:])[1:] | |||
| @@ -72,7 +72,7 @@ class Watcher(IRCConnection): | |||
| # When we've finished starting up, join all watcher channels: | |||
| elif line[1] == "376": | |||
| for chan in config.irc["watcher"]["channels"]: | |||
| for chan in self.config.irc["watcher"]["channels"]: | |||
| self.join(chan) | |||
| def _prepare_process_hook(self): | |||
| @@ -84,12 +84,12 @@ class Watcher(IRCConnection): | |||
| # Set a default RC process hook that does nothing: | |||
| self._process_hook = lambda rc: () | |||
| try: | |||
| rules = config.data["rules"] | |||
| rules = self.config.data["rules"] | |||
| except KeyError: | |||
| return | |||
| module = imp.new_module("_rc_event_processing_rules") | |||
| try: | |||
| exec compile(rules, config.path, "exec") in module.__dict__ | |||
| exec compile(rules, self.config.path, "exec") in module.__dict__ | |||
| except Exception: | |||
| e = "Could not compile config file's RC event rules" | |||
| self.logger.exception(e) | |||
| @@ -1,132 +0,0 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot's Main Module | |||
| The core is essentially responsible for starting the various bot components | |||
| (irc, scheduler, etc) and making sure they are all happy. An explanation of the | |||
| different components follows: | |||
| EarwigBot has three components that can run independently of each other: an IRC | |||
| front-end, an IRC watcher, and a wiki scheduler. | |||
| * The IRC front-end runs on a normal IRC server and expects users to interact | |||
| with it/give it commands. | |||
| * The IRC watcher runs on a wiki recent-changes server and listens for edits. | |||
| Users cannot interact with this part of the bot. | |||
| * The wiki scheduler runs wiki-editing bot tasks in separate threads at | |||
| user-defined times through a cron-like interface. | |||
| There is a "priority" system here: | |||
| 1. If the IRC frontend is enabled, it will run on the main thread, and the IRC | |||
| watcher and wiki scheduler (if enabled) will run on separate threads. | |||
| 2. If the wiki scheduler is enabled, it will run on the main thread, and the | |||
| IRC watcher (if enabled) will run on a separate thread. | |||
| 3. If the IRC watcher is enabled, it will run on the main (and only) thread. | |||
| Else, the bot will stop, as no components are enabled. | |||
| """ | |||
| import logging | |||
| import threading | |||
| import time | |||
| from earwigbot.config import config | |||
| from earwigbot.irc import Frontend, Watcher | |||
| from earwigbot.tasks import task_manager | |||
| logger = logging.getLogger("earwigbot") | |||
| def irc_watcher(frontend=None): | |||
| """Function to handle the IRC watcher as another thread (if frontend and/or | |||
| scheduler is enabled), otherwise run as the main thread.""" | |||
| while 1: # Restart the watcher component if it breaks (and nothing else) | |||
| watcher = Watcher(frontend) | |||
| try: | |||
| watcher.loop() | |||
| except: | |||
| logger.exception("Watcher had an error") | |||
| time.sleep(5) # Sleep a bit before restarting watcher | |||
| logger.warn("Watcher has stopped; restarting component") | |||
| def wiki_scheduler(): | |||
| """Function to handle the wiki scheduler as another thread, or as the | |||
| primary thread if the IRC frontend is not enabled.""" | |||
| while 1: | |||
| time_start = time.time() | |||
| task_manager.schedule() | |||
| time_end = time.time() | |||
| time_diff = time_start - time_end | |||
| if time_diff < 60: # Sleep until the next minute | |||
| time.sleep(60 - time_diff) | |||
| def irc_frontend(): | |||
| """If the IRC frontend is enabled, make it run on our primary thread, and | |||
| enable the wiki scheduler and IRC watcher on new threads if they are | |||
| enabled.""" | |||
| logger.info("Starting IRC frontend") | |||
| frontend = Frontend() | |||
| if config.components.get("wiki_schedule"): | |||
| logger.info("Starting wiki scheduler") | |||
| task_manager.load() | |||
| t_scheduler = threading.Thread(target=wiki_scheduler) | |||
| t_scheduler.name = "wiki-scheduler" | |||
| t_scheduler.daemon = True | |||
| t_scheduler.start() | |||
| if config.components.get("irc_watcher"): | |||
| logger.info("Starting IRC watcher") | |||
| t_watcher = threading.Thread(target=irc_watcher, args=(frontend,)) | |||
| t_watcher.name = "irc-watcher" | |||
| t_watcher.daemon = True | |||
| t_watcher.start() | |||
| frontend.loop() | |||
| def main(): | |||
| if config.components.get("irc_frontend"): | |||
| # Make the frontend run on our primary thread if enabled, and enable | |||
| # additional components through that function: | |||
| irc_frontend() | |||
| elif config.components.get("wiki_schedule"): | |||
| # Run the scheduler on the main thread, but also run the IRC watcher on | |||
| # another thread iff it is enabled: | |||
| logger.info("Starting wiki scheduler") | |||
| task_manager.load() | |||
| if "irc_watcher" in enabled: | |||
| logger.info("Starting IRC watcher") | |||
| t_watcher = threading.Thread(target=irc_watcher) | |||
| t_watcher.name = "irc-watcher" | |||
| t_watcher.daemon = True | |||
| t_watcher.start() | |||
| wiki_scheduler() | |||
| elif config.components.get("irc_watcher"): | |||
| # The IRC watcher is our only enabled component, so run its function | |||
| # only and don't worry about anything else: | |||
| logger.info("Starting IRC watcher") | |||
| irc_watcher() | |||
| else: # Nothing is enabled! | |||
| logger.critical("No bot parts are enabled; stopping") | |||
| exit(1) | |||
| @@ -1,65 +0,0 @@ | |||
| #! /usr/bin/env python | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot Runner | |||
| This is a very simple script that can be run from anywhere. It will add the | |||
| 'earwigbot' package to sys.path if it's not already in there (i.e., it hasn't | |||
| been "installed"), accept a root_dir (the directory in which bot.py is located) | |||
| and a decryption key from raw_input (if passwords are encrypted), then call | |||
| config.load() and decrypt any passwords, and finally call the main() function | |||
| of earwigbot.main. | |||
| """ | |||
| from os import path | |||
| import sys | |||
| def run(): | |||
| pkg_dir = path.split(path.dirname(path.abspath(__file__)))[0] | |||
| if pkg_dir not in sys.path: | |||
| sys.path.insert(0, pkg_dir) | |||
| from earwigbot.config import config | |||
| from earwigbot import main | |||
| root_dir = raw_input() | |||
| config_path = path.join(root_dir, "config.yml") | |||
| log_dir = path.join(root_dir, "logs") | |||
| is_encrypted = config.load(config_path, log_dir) | |||
| if is_encrypted: | |||
| config._decryption_key = raw_input() | |||
| config.decrypt(config.wiki, "password") | |||
| config.decrypt(config.wiki, "search", "credentials", "key") | |||
| config.decrypt(config.wiki, "search", "credentials", "secret") | |||
| config.decrypt(config.irc, "frontend", "nickservPassword") | |||
| config.decrypt(config.irc, "watcher", "nickservPassword") | |||
| try: | |||
| main.main() | |||
| except KeyboardInterrupt: | |||
| main.logger.critical("KeyboardInterrupt: stopping main bot loop") | |||
| exit(1) | |||
| if __name__ == "__main__": | |||
| run() | |||
| @@ -213,10 +213,6 @@ class _TaskManager(object): | |||
| task_thread = threading.Thread(target=func) | |||
| start_time = time.strftime("%b %d %H:%M:%S") | |||
| task_thread.name = "{0} ({1})".format(task_name, start_time) | |||
| # Stop bot task threads automagically if the main bot stops: | |||
| task_thread.daemon = True | |||
| task_thread.start() | |||
| def get(self, task_name): | |||
| @@ -0,0 +1,50 @@ | |||
| #! /usr/bin/env python | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| import argparse | |||
| from os import path | |||
| from earwigbot import __version__ | |||
| from earwigbot.bot import Bot | |||
| class BotUtility(object): | |||
| """ | |||
| DOCSTRING NEEDED | |||
| """ | |||
| def version(self): | |||
| return __version__ | |||
| def run(self): | |||
| print "EarwigBot v{0}\n".format(self.version()) | |||
| def main(self): | |||
| root_dir = path.abspath(path.curdir()) | |||
| bot = Bot(root_dir) | |||
| bot.run() | |||
| main = BotUtility().main | |||
| if __name__ == "__main__": | |||
| main() | |||
| @@ -0,0 +1,40 @@ | |||
| #! /usr/bin/env python | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| DOCSTRING NEEDED | |||
| """ | |||
| from setuptools import setup | |||
| setup( | |||
| name = "earwigbot", | |||
| entry_points = {"console_scripts": ["earwigbot = earwigbot.util:main"]}, | |||
| install_requires = ["PyYAML>=3.10", "oursql>=0.9.3", "oauth2>=1.5.211", | |||
| "numpy>=1.6.1", "matplotlib>=1.1.0"], | |||
| version = "0.1.dev", | |||
| author = "Ben Kurtovic", | |||
| author_email = "ben.kurtovic@verizon.net", | |||
| license = "MIT License", | |||
| url = "https://github.com/earwig/earwigbot", | |||
| ) | |||
| @@ -23,7 +23,7 @@ | |||
| """ | |||
| EarwigBot's Unit Tests | |||
| This module __init__ file provides some support code for unit tests. | |||
| This package __init__ file provides some support code for unit tests. | |||
| CommandTestCase is a subclass of unittest.TestCase that provides setUp() for | |||
| creating a fake connection and some other helpful methods. It uses | |||
| @@ -92,6 +92,7 @@ class CommandTestCase(TestCase): | |||
| line = ":Foo!bar@example.com JOIN :#channel".strip().split() | |||
| return self.maker(line, line[2][1:]) | |||
| class FakeConnection(IRCConnection): | |||
| def __init__(self): | |||
| pass | |||