@@ -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 |