Переглянути джерело

Beginning work (#16)

tags/v0.1^2
Ben Kurtovic 12 роки тому
джерело
коміт
117eccc35d
19 змінених файлів з 353 додано та 422 видалено
  1. +0
    -6
      .gitignore
  2. +0
    -70
      bot.py
  3. +1
    -3
      earwigbot/__init__.py
  4. +113
    -0
      earwigbot/bot.py
  5. +1
    -1
      earwigbot/commands/restart.py
  6. +3
    -13
      earwigbot/commands/threads.py
  7. +105
    -108
      earwigbot/config.py
  8. +26
    -7
      earwigbot/irc/connection.py
  9. +6
    -6
      earwigbot/irc/frontend.py
  10. +6
    -6
      earwigbot/irc/watcher.py
  11. +0
    -132
      earwigbot/main.py
  12. +0
    -65
      earwigbot/runner.py
  13. +0
    -4
      earwigbot/tasks/__init__.py
  14. +50
    -0
      earwigbot/util.py
  15. +40
    -0
      setup.py
  16. +2
    -1
      tests/__init__.py
  17. +0
    -0
      tests/test_blowfish.py
  18. +0
    -0
      tests/test_calc.py
  19. +0
    -0
      tests/test_test.py

+ 0
- 6
.gitignore Переглянути файл

@@ -1,9 +1,3 @@
# Ignore bot-specific files:
logs/
config.yml
sites.db
.cookies

# Ignore python bytecode:
*.pyc



+ 0
- 70
bot.py Переглянути файл

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

+ 1
- 3
earwigbot/__init__.py Переглянути файл

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

+ 113
- 0
earwigbot/bot.py Переглянути файл

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

+ 1
- 1
earwigbot/commands/restart.py Переглянути файл

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

+ 3
- 13
earwigbot/commands/threads.py Переглянути файл

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

+ 105
- 108
earwigbot/config.py Переглянути файл

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

+ 26
- 7
earwigbot/irc/connection.py Переглянути файл

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

+ 6
- 6
earwigbot/irc/frontend.py Переглянути файл

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

+ 6
- 6
earwigbot/irc/watcher.py Переглянути файл

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


+ 0
- 132
earwigbot/main.py Переглянути файл

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

+ 0
- 65
earwigbot/runner.py Переглянути файл

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

+ 0
- 4
earwigbot/tasks/__init__.py Переглянути файл

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


+ 50
- 0
earwigbot/util.py Переглянути файл

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

+ 40
- 0
setup.py Переглянути файл

@@ -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",
)

earwigbot/tests/__init__.py → tests/__init__.py Переглянути файл

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

earwigbot/tests/test_blowfish.py → tests/test_blowfish.py Переглянути файл


earwigbot/tests/test_calc.py → tests/test_calc.py Переглянути файл


earwigbot/tests/test_test.py → tests/test_test.py Переглянути файл


Завантаження…
Відмінити
Зберегти