| @@ -1,9 +1,3 @@ | |||||
| # Ignore bot-specific files: | |||||
| logs/ | |||||
| config.yml | |||||
| sites.db | |||||
| .cookies | |||||
| # Ignore python bytecode: | # Ignore python bytecode: | ||||
| *.pyc | *.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" | __version__ = "0.1.dev" | ||||
| __email__ = "ben.kurtovic@verizon.net" | __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 | return | ||||
| self.connection.logger.info("Restarting bot per owner request") | 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: | for thread in threads: | ||||
| tname = thread.name | tname = thread.name | ||||
| if tname == "MainThread": | 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})" | t = "\x0302{0}\x0301 (id {1})" | ||||
| normal_threads.append(t.format(tname, thread.ident)) | normal_threads.append(t.format(tname, thread.ident)) | ||||
| elif tname.startswith("reminder"): | elif tname.startswith("reminder"): | ||||
| @@ -157,12 +156,3 @@ class Command(BaseCommand): | |||||
| task_manager.start(task_name, **data.kwargs) | task_manager.start(task_name, **data.kwargs) | ||||
| msg = "task \x0302{0}\x0301 started.".format(task_name) | msg = "task \x0302{0}\x0301 started.".format(task_name) | ||||
| self.connection.reply(data, msg) | 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 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| # SOFTWARE. | # 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 | ||||
| import logging.handlers | import logging.handlers | ||||
| from os import mkdir, path | from os import mkdir, path | ||||
| @@ -53,44 +29,36 @@ import yaml | |||||
| from earwigbot import blowfish | 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._config_path = path.join(self._root_dir, "config.yml") | ||||
| self._log_dir = path.join(self._root_dir, "logs") | self._log_dir = path.join(self._root_dir, "logs") | ||||
| self._decryption_key = None | self._decryption_key = None | ||||
| @@ -104,17 +72,17 @@ class _BotConfig(object): | |||||
| self._nodes = [self._components, self._wiki, self._tasks, self._irc, | self._nodes = [self._components, self._wiki, self._tasks, self._irc, | ||||
| self._metadata] | self._metadata] | ||||
| self._decryptable_nodes = [] | |||||
| def _load(self): | 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 | filename = self._config_path | ||||
| with open(filename, 'r') as fp: | with open(filename, 'r') as fp: | ||||
| try: | try: | ||||
| self._data = yaml.load(fp) | self._data = yaml.load(fp) | ||||
| except yaml.YAMLError as error: | except yaml.YAMLError as error: | ||||
| print "Error parsing config file {0}:".format(filename) | print "Error parsing config file {0}:".format(filename) | ||||
| print error | |||||
| exit(1) | |||||
| raise | |||||
| def _setup_logging(self): | def _setup_logging(self): | ||||
| """Configures the logging module so it works the way we want it to.""" | """Configures the logging module so it works the way we want it to.""" | ||||
| @@ -135,7 +103,7 @@ class _BotConfig(object): | |||||
| else: | else: | ||||
| msg = "log_dir ({0}) exists but is not a directory!" | msg = "log_dir ({0}) exists but is not a directory!" | ||||
| print msg.format(log_dir) | print msg.format(log_dir) | ||||
| exit(1) | |||||
| return | |||||
| main_handler = hand(logfile("bot.log"), "midnight", 1, 7) | main_handler = hand(logfile("bot.log"), "midnight", 1, 7) | ||||
| error_handler = hand(logfile("error.log"), "W6", 1, 4) | error_handler = hand(logfile("error.log"), "W6", 1, 4) | ||||
| @@ -149,27 +117,29 @@ class _BotConfig(object): | |||||
| h.setFormatter(formatter) | h.setFormatter(formatter) | ||||
| logger.addHandler(h) | 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): | def _make_new(self): | ||||
| """Make a new config file based on the user's input.""" | """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 | @property | ||||
| def root_dir(self): | def root_dir(self): | ||||
| @@ -182,7 +152,7 @@ class _BotConfig(object): | |||||
| @property | @property | ||||
| def log_dir(self): | def log_dir(self): | ||||
| return self._log_dir | return self._log_dir | ||||
| @property | @property | ||||
| def data(self): | def data(self): | ||||
| """The entire config file.""" | """The entire config file.""" | ||||
| @@ -221,7 +191,7 @@ class _BotConfig(object): | |||||
| """Return True if passwords are encrypted, otherwise False.""" | """Return True if passwords are encrypted, otherwise False.""" | ||||
| return self.metadata.get("encryptPasswords", 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. | """Load, or reload, our config file. | ||||
| First, check if we have a valid config file, and if not, notify the | 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 | wiki, tasks, irc, metadata) for easy access (as well as the internal | ||||
| _data variable). | _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): | if not path.exists(self._config_path): | ||||
| print "You haven't configured the bot yet!" | print "You haven't configured the bot yet!" | ||||
| choice = raw_input("Would you like to do this now? [y/n] ") | choice = raw_input("Would you like to do this now? [y/n] ") | ||||
| if choice.lower().startswith("y"): | if choice.lower().startswith("y"): | ||||
| return self._make_new() | |||||
| self._make_new() | |||||
| else: | else: | ||||
| exit(1) | exit(1) | ||||
| @@ -257,25 +222,28 @@ class _BotConfig(object): | |||||
| self.metadata._load(data.get("metadata", {})) | self.metadata._load(data.get("metadata", {})) | ||||
| self._setup_logging() | 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): | def decrypt(self, node, *nodes): | ||||
| """Use self._decryption_key to decrypt an object in our config tree. | """Use self._decryption_key to decrypt an object in our config tree. | ||||
| If this is called when passwords are not encrypted (check with | 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") | 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): | def schedule(self, minute, hour, month_day, month, week_day): | ||||
| """Return a list of tasks scheduled to run at the specified time. | """Return a list of tasks scheduled to run at the specified time. | ||||
| @@ -311,6 +279,38 @@ class _BotConfig(object): | |||||
| return tasks | 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): | class _BotFormatter(logging.Formatter): | ||||
| def __init__(self, color=False): | def __init__(self, color=False): | ||||
| self._format = super(_BotFormatter, self).format | self._format = super(_BotFormatter, self).format | ||||
| @@ -336,6 +336,3 @@ class _BotFormatter(logging.Formatter): | |||||
| if record.levelno == logging.CRITICAL: | if record.levelno == logging.CRITICAL: | ||||
| record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red | record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red | ||||
| return record | return record | ||||
| config = _BotConfig() | |||||
| @@ -22,6 +22,7 @@ | |||||
| import socket | import socket | ||||
| import threading | import threading | ||||
| from time import sleep | |||||
| __all__ = ["BrokenSocketException", "IRCConnection"] | __all__ = ["BrokenSocketException", "IRCConnection"] | ||||
| @@ -42,7 +43,7 @@ class IRCConnection(object): | |||||
| self.ident = ident | self.ident = ident | ||||
| self.realname = realname | self.realname = realname | ||||
| self.logger = logger | self.logger = logger | ||||
| self.is_running = False | |||||
| self._is_running = False | |||||
| # A lock to prevent us from sending two messages at once: | # A lock to prevent us from sending two messages at once: | ||||
| self._lock = threading.Lock() | self._lock = threading.Lock() | ||||
| @@ -53,8 +54,9 @@ class IRCConnection(object): | |||||
| try: | try: | ||||
| self._sock.connect((self.host, self.port)) | self._sock.connect((self.host, self.port)) | ||||
| except socket.error: | 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("NICK {0}".format(self.nick)) | ||||
| self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname)) | 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): | def _get(self, size=4096): | ||||
| """Receive (i.e. get) data from the server.""" | """Receive (i.e. get) data from the server.""" | ||||
| data = self._sock.recv(4096) | |||||
| data = self._sock.recv(size) | |||||
| if not data: | if not data: | ||||
| # Socket isn't giving us any data, so it is dead or broken: | # Socket isn't giving us any data, so it is dead or broken: | ||||
| raise BrokenSocketException() | raise BrokenSocketException() | ||||
| @@ -121,21 +123,38 @@ class IRCConnection(object): | |||||
| msg = "PONG {0}".format(target) | msg = "PONG {0}".format(target) | ||||
| self._send(msg) | 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): | def loop(self): | ||||
| """Main loop for the IRC connection.""" | """Main loop for the IRC connection.""" | ||||
| self.is_running = True | |||||
| self._is_running = True | |||||
| read_buffer = "" | read_buffer = "" | ||||
| while 1: | while 1: | ||||
| try: | try: | ||||
| read_buffer += self._get() | read_buffer += self._get() | ||||
| except BrokenSocketException: | except BrokenSocketException: | ||||
| self.is_running = False | |||||
| self._is_running = False | |||||
| break | break | ||||
| lines = read_buffer.split("\n") | lines = read_buffer.split("\n") | ||||
| read_buffer = lines.pop() | read_buffer = lines.pop() | ||||
| for line in lines: | for line in lines: | ||||
| self._process_message(line) | self._process_message(line) | ||||
| if not self.is_running: | |||||
| if self.is_stopped(): | |||||
| self._close() | self._close() | ||||
| break | 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.commands import command_manager | ||||
| from earwigbot.irc import IRCConnection, Data, BrokenSocketException | from earwigbot.irc import IRCConnection, Data, BrokenSocketException | ||||
| from earwigbot.config import config | |||||
| __all__ = ["Frontend"] | __all__ = ["Frontend"] | ||||
| @@ -41,7 +40,8 @@ class Frontend(IRCConnection): | |||||
| """ | """ | ||||
| sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | ||||
| def __init__(self): | |||||
| def __init__(self, config): | |||||
| self.config = config | |||||
| self.logger = logging.getLogger("earwigbot.frontend") | self.logger = logging.getLogger("earwigbot.frontend") | ||||
| cf = config.irc["frontend"] | cf = config.irc["frontend"] | ||||
| base = super(Frontend, self) | base = super(Frontend, self) | ||||
| @@ -66,7 +66,7 @@ class Frontend(IRCConnection): | |||||
| data.msg = " ".join(line[3:])[1:] | data.msg = " ".join(line[3:])[1:] | ||||
| data.chan = line[2] | 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 | # This is a privmsg to us, so set 'chan' as the nick of the | ||||
| # sender, then check for private-only command hooks: | # sender, then check for private-only command hooks: | ||||
| data.chan = data.nick | data.chan = data.nick | ||||
| @@ -86,8 +86,8 @@ class Frontend(IRCConnection): | |||||
| elif line[1] == "376": | elif line[1] == "376": | ||||
| # If we're supposed to auth to NickServ, do that: | # If we're supposed to auth to NickServ, do that: | ||||
| try: | 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: | except KeyError: | ||||
| pass | pass | ||||
| else: | else: | ||||
| @@ -95,5 +95,5 @@ class Frontend(IRCConnection): | |||||
| self.say("NickServ", msg) | self.say("NickServ", msg) | ||||
| # Join all of our startup channels: | # Join all of our startup channels: | ||||
| for chan in config.irc["frontend"]["channels"]: | |||||
| for chan in self.config.irc["frontend"]["channels"]: | |||||
| self.join(chan) | self.join(chan) | ||||
| @@ -24,7 +24,6 @@ import imp | |||||
| import logging | import logging | ||||
| from earwigbot.irc import IRCConnection, RC, BrokenSocketException | from earwigbot.irc import IRCConnection, RC, BrokenSocketException | ||||
| from earwigbot.config import config | |||||
| __all__ = ["Watcher"] | __all__ = ["Watcher"] | ||||
| @@ -39,7 +38,8 @@ class Watcher(IRCConnection): | |||||
| to channels on the IRC frontend. | 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") | self.logger = logging.getLogger("earwigbot.watcher") | ||||
| cf = config.irc["watcher"] | cf = config.irc["watcher"] | ||||
| base = super(Watcher, self) | base = super(Watcher, self) | ||||
| @@ -58,7 +58,7 @@ class Watcher(IRCConnection): | |||||
| # Ignore messages originating from channels not in our list, to | # Ignore messages originating from channels not in our list, to | ||||
| # prevent someone PMing us false data: | # prevent someone PMing us false data: | ||||
| if chan not in config.irc["watcher"]["channels"]: | |||||
| if chan not in self.config.irc["watcher"]["channels"]: | |||||
| return | return | ||||
| msg = " ".join(line[3:])[1:] | msg = " ".join(line[3:])[1:] | ||||
| @@ -72,7 +72,7 @@ class Watcher(IRCConnection): | |||||
| # When we've finished starting up, join all watcher channels: | # When we've finished starting up, join all watcher channels: | ||||
| elif line[1] == "376": | elif line[1] == "376": | ||||
| for chan in config.irc["watcher"]["channels"]: | |||||
| for chan in self.config.irc["watcher"]["channels"]: | |||||
| self.join(chan) | self.join(chan) | ||||
| def _prepare_process_hook(self): | def _prepare_process_hook(self): | ||||
| @@ -84,12 +84,12 @@ class Watcher(IRCConnection): | |||||
| # Set a default RC process hook that does nothing: | # Set a default RC process hook that does nothing: | ||||
| self._process_hook = lambda rc: () | self._process_hook = lambda rc: () | ||||
| try: | try: | ||||
| rules = config.data["rules"] | |||||
| rules = self.config.data["rules"] | |||||
| except KeyError: | except KeyError: | ||||
| return | return | ||||
| module = imp.new_module("_rc_event_processing_rules") | module = imp.new_module("_rc_event_processing_rules") | ||||
| try: | try: | ||||
| exec compile(rules, config.path, "exec") in module.__dict__ | |||||
| exec compile(rules, self.config.path, "exec") in module.__dict__ | |||||
| except Exception: | except Exception: | ||||
| e = "Could not compile config file's RC event rules" | e = "Could not compile config file's RC event rules" | ||||
| self.logger.exception(e) | 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) | task_thread = threading.Thread(target=func) | ||||
| start_time = time.strftime("%b %d %H:%M:%S") | start_time = time.strftime("%b %d %H:%M:%S") | ||||
| task_thread.name = "{0} ({1})".format(task_name, start_time) | 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() | task_thread.start() | ||||
| def get(self, task_name): | 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 | 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 | CommandTestCase is a subclass of unittest.TestCase that provides setUp() for | ||||
| creating a fake connection and some other helpful methods. It uses | 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() | line = ":Foo!bar@example.com JOIN :#channel".strip().split() | ||||
| return self.maker(line, line[2][1:]) | return self.maker(line, line[2][1:]) | ||||
| class FakeConnection(IRCConnection): | class FakeConnection(IRCConnection): | ||||
| def __init__(self): | def __init__(self): | ||||
| pass | pass | ||||