Browse Source

tons of improvements, import fixes, cleanup, etc

tags/v0.1^2
Ben Kurtovic 13 years ago
parent
commit
b0da4531b2
9 changed files with 220 additions and 199 deletions
  1. +3
    -3
      bot/classes/data.py
  2. +32
    -28
      bot/commands/__init__.py
  3. +2
    -3
      bot/config.py
  4. +71
    -63
      bot/frontend.py
  5. +7
    -13
      bot/main.py
  6. +42
    -39
      bot/tasks/__init__.py
  7. +50
    -37
      bot/watcher.py
  8. +10
    -10
      bot/watcher_logic.py
  9. +3
    -3
      earwigbot.py

+ 3
- 3
bot/classes/data.py View File

@@ -11,9 +11,9 @@ class KwargParseException(Exception):
pass

class Data(object):
def __init__(self):
"""store data from an individual line received on IRC"""
self.line = str()
def __init__(self, line):
"""Store data from an individual line received on IRC."""
self.line = line
self.chan = str()
self.nick = str()
self.ident = str()


+ 32
- 28
bot/commands/__init__.py View File

@@ -5,29 +5,11 @@
import os
import traceback

commands = []
__all__ = ["load", "get_all", "check"]

def load_commands(connection):
"""load all valid command classes from irc/commmands/ into the commands variable"""
files = os.listdir(os.path.join("irc", "commands")) # get all files in irc/commands/
files.sort() # alphabetically sort list of files
_commands = []

for f in files:
if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_"
continue
module = f[:-3] # strip .py from end
try:
exec "from irc.commands import %s" % module
except: # importing the file failed for some reason...
print "Couldn't load file %s:" % f
traceback.print_exc()
continue
process_module(connection, eval(module)) # 'module' is a string, so get the actual object for processing by eval-ing it

pretty_cmnds = map(lambda c: c.__class__.__name__, commands)
print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds))

def process_module(connection, module):
def _process_module(connection, module):
"""go through all objects in a module and add valid command classes to the commands variable"""
global commands
objects = dir(module)
@@ -43,19 +25,41 @@ def process_module(connection, module):
for base in bases:
if base.__name__ == "BaseCommand": # this inherits BaseCommand, so it must be a command class
command = obj(connection) # initialize a new command object
commands.append(command)
_commands.append(command)
print "Added command class %s from %s..." % (this_obj, module.__name__)
continue

def get_commands():
"""get our commands"""
return commands
def load(connection):
"""load all valid command classes from irc/commmands/ into the commands variable"""
files = os.listdir(os.path.join("irc", "commands")) # get all files in irc/commands/
files.sort() # alphabetically sort list of files

for f in files:
if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_"
continue
module = f[:-3] # strip .py from end
try:
exec "from irc.commands import %s" % module
except: # importing the file failed for some reason...
print "Couldn't load file %s:" % f
traceback.print_exc()
continue
process_module(connection, eval(module)) # 'module' is a string, so get the actual object for processing by eval-ing it

pretty_cmnds = map(lambda c: c.__class__.__name__, commands)
print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds))

def get_all():
"""Return our list of all commands."""
return _commands

def check(hook, data):
"""given an event on IRC, check if there's anything we can respond to by calling each command class"""
data.parse_args() # parse command arguments into data.command and data.args
"""Given an event on IRC, check if there's anything we can respond to by
calling each command class"""
# parse command arguments into data.command and data.args
data.parse_args()

for command in commands:
for command in _commands:
if hook in command.get_hooks():
if command.check(data):
try:


+ 2
- 3
bot/config.py View File

@@ -18,9 +18,9 @@ from within config's three global variables and one function:
"""

import json
from os import makedirs, path
from os import path

from lib import blowfish
import blowfish

script_dir = path.dirname(path.abspath(__file__))
root_dir = path.split(script_dir)[0]
@@ -149,7 +149,6 @@ def schedule(minute, hour, month_day, month, week_day):

def make_new_config():
"""Make a new config file based on the user's input."""
makedirs(config_dir)

encrypt = raw_input("Would you like to encrypt passwords stored in " +
"config.json? [y/n] ")


+ 71
- 63
bot/frontend.py View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's IRC Front-end Component
EarwigBot's IRC Frontend Component

The IRC frontend runs on a normal IRC server and expects users to interact with
it and give it commands. Commands are stored as "command classes", subclasses
@@ -9,13 +9,16 @@ of BaseCommand in irc/base_command.py. All command classes are automatically
imported by irc/command_handler.py if they are in irc/commands.
"""

from re import findall
import re

from core import config
from irc import command_handler
from irc.classes import Connection, Data, BrokenSocketException
import config
import commands
from classes import Connection, Data, BrokenSocketException

__all__ = ["get_connection", "startup", "main"]

connection = None
sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z")

def get_connection():
"""Return a new Connection() instance with information about our server
@@ -31,16 +34,18 @@ def startup(conn):
command_handler, and then establish a connection with the IRC server."""
global connection
connection = conn
command_handler.load_commands(connection)
commands.load(connection)
connection.connect()

def main():
"""Main loop for the Frontend IRC Bot component. get_connection() and
startup() should have already been called."""
"""Main loop for the frontend component.

get_connection() and startup() should have already been called before this.
"""
read_buffer = str()

while 1:
try:
try:
read_buffer = read_buffer + connection.get()
except BrokenSocketException:
print "Socket has broken on front-end; restarting bot..."
@@ -48,57 +53,60 @@ def main():

lines = read_buffer.split("\n")
read_buffer = lines.pop()

for line in lines: # handle a single message from IRC
line = line.strip().split()
data = Data() # new Data() instance to store info about this line
data.line = line

if line[1] == "JOIN":
data.nick, data.ident, data.host = findall(
":(.*?)!(.*?)@(.*?)\Z", line[0])[0]
data.chan = line[2][1:]
command_handler.check("join", data) # check for 'join' hooks in
# our commands

if line[1] == "PRIVMSG":
data.nick, data.ident, data.host = findall(
":(.*?)!(.*?)@(.*?)\Z", line[0])[0]
data.msg = ' '.join(line[3:])[1:]
data.chan = line[2]

if data.chan == 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
command_handler.check("msg_private", data)
else:
# check for public-only command hooks
command_handler.check("msg_public", data)

# check for command hooks that apply to all messages
command_handler.check("msg", data)

# hardcode the !restart command (we can't restart from within
# an ordinary command)
if data.msg in ["!restart", ".restart"]:
if data.host in config.irc["permissions"]["owners"]:
print "Restarting bot per owner request..."
return

if line[0] == "PING": # if we are pinged, pong back to the server
connection.send("PONG %s" % line[1])

if line[1] == "376": # we've successfully connected to the network
try: # if we're supposed to auth to nickserv, do that
ns_username = config.irc["frontend"]["nickservUsername"]
ns_password = config.irc["frontend"]["nickservPassword"]
except KeyError:
pass
else:
connection.say("NickServ", "IDENTIFY {0} {1}".format(
ns_username, ns_password))
# join all of our startup channels
for chan in config.irc["frontend"]["channels"]:
connection.join(chan)
for line in lines:
_process_message(line)

def _process_message(line):
"""Process a single message from IRC."""
line = line.strip().split()
data = Data(line) # new Data instance to store info about this line

if line[1] == "JOIN":
data.nick, data.ident, data.host = sender_regex.findall(line[0])[0]
data.chan = line[2][1:]
# Check for 'join' hooks in our commands:
commands.check("join", data)

elif line[1] == "PRIVMSG":
data.nick, data.ident, data.host = sender_regex.findall(line[0])[0]
data.msg = ' '.join(line[3:])[1:]
data.chan = line[2]

if data.chan == 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
commands.check("msg_private", data)
else:
# Check for public-only command hooks:
commands.check("msg_public", data)

# Check for command hooks that apply to all messages:
commands.check("msg", data)

# Hardcode the !restart command (we can't restart from within an
# ordinary command):
if data.msg in ["!restart", ".restart"]:
if data.host in config.irc["permissions"]["owners"]:
print "Restarting bot per owner request..."
return

# If we are pinged, pong back to the server:
if line[0] == "PING":
connection.send("PONG %s" % line[1])

# On successful connection to the server:
if line[1] == "376":
# If we're supposed to auth to NickServ, do that:
try:
username = config.irc["frontend"]["nickservUsername"]
password = config.irc["frontend"]["nickservPassword"]
except KeyError:
pass
else:
msg = " ".join(("IDENTIFY", username, password))
connection.say("NickServ", msg)

# Join all of our startup channels:
for chan in config.irc["frontend"]["channels"]:
connection.join(chan)

+ 7
- 13
bot/main.py View File

@@ -33,17 +33,11 @@ Else, the bot will stop, as no components are enabled.
import threading
import time
import traceback
import sys
import os

script_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.split(script_dir)[0] # the bot's "root" directory relative
# to its different components
sys.path.append(root_dir) # make sure we look in the root dir for modules

from core import config
from irc import frontend, watcher
from wiki import task_manager
import config
import frontend
import tasks
import watcher

f_conn = None
w_conn = None
@@ -70,7 +64,7 @@ def wiki_scheduler():
time_start = time.time()
now = time.gmtime(time_start)

task_manager.start_tasks(now)
tasks.schedule(now)

time_end = time.time()
time_diff = time_start - time_end
@@ -89,7 +83,7 @@ def irc_frontend():

if "wiki_schedule" in config.components:
print "\nStarting wiki scheduler..."
task_manager.load_tasks()
tasks.load()
t_scheduler = threading.Thread(target=wiki_scheduler)
t_scheduler.name = "wiki-scheduler"
t_scheduler.daemon = True
@@ -123,7 +117,7 @@ def run():

elif "wiki_schedule" in enabled: # run the scheduler on the main
print "Starting wiki scheduler..." # thread, but also run the IRC
task_manager.load_tasks() # watcher on another thread iff it
tasks.load() # watcher on another thread iff it
if "irc_watcher" in enabled: # is enabled
print "\nStarting IRC watcher..."
t_watcher = threading.Thread(target=irc_watcher, args=())


+ 42
- 39
bot/tasks/__init__.py View File

@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's Wiki Bot Task Manager
EarwigBot's Wiki Task Manager

This module provides some functions to run and load bot tasks from wiki/tasks/.
This package provides the wiki bot "tasks" EarwigBot runs. Here in __init__,
you can find some functions used to load and run these tasks.
"""

import time
@@ -11,28 +12,17 @@ import traceback
import threading
import os

from core import config
import config

__all__ = ["load", "schedule", "start"]

# store loaded tasks as a dict where the key is the task name and the value is
# an instance of the task class (wiki.tasks.task_file.Task())
task_list = dict()
_tasks = dict()

def load_tasks():
"""Load all valid task classes from wiki/tasks/, and add them to the
task_list variable."""
files = os.listdir(os.path.join("wiki", "tasks"))
files.sort() # alphabetically sort all files in wiki/tasks/
for f in files:
if not os.path.isfile(os.path.join("wiki", "tasks", f)):
continue # ignore non-files
if f.startswith("_") or not f.endswith(".py"):
continue # ignore non-python files or files beginning with an _
load_class_from_file(f)
print "Found %s tasks: %s." % (len(task_list), ', '.join(task_list.keys()))

def load_class_from_file(f):
def _load_class_from_file(f):
"""Look in a given file for the task class."""
global task_list
global _tasks

module = f[:-3] # strip .py from end
try:
@@ -48,45 +38,58 @@ def load_class_from_file(f):
traceback.print_exc()
return
task_name = task_class.task_name
task_list[task_name] = task_class()
print "Added task %s from wiki/tasks/%s..." % (task_name, f)
_tasks[task_name] = task_class()
print "Added task %s from bot/tasks/%s..." % (task_name, f)

def _wrapper(task, **kwargs):
"""Wrapper for task classes: run the task and catch any errors."""
try:
task.run(**kwargs)
except:
print "Task '{0}' raised an exception and had to stop:".format(task.task_name)
traceback.print_exc()
else:
print "Task '{0}' finished without error.".format(task.task_name)

def start_tasks(now=time.gmtime()):
def load():
"""Load all valid task classes from bot/tasks/, and add them to the
_tasks variable."""
files = os.listdir(os.path.join("bot", "tasks"))
files.sort() # alphabetically sort all files in wiki/tasks/
for f in files:
if not os.path.isfile(os.path.join("bot", "tasks", f)):
continue # ignore non-files
if f.startswith("_") or not f.endswith(".py"):
continue # ignore non-python files or files beginning with an _
load_class_from_file(f)
print "Found %s tasks: %s." % (len(_tasks), ', '.join(_tasks.keys()))

def schedule(now=time.gmtime()):
"""Start all tasks that are supposed to be run at a given time."""
tasks = config.schedule(now.tm_min, now.tm_hour, now.tm_mday, now.tm_mon,
now.tm_wday) # get list of tasks to run this turn

for task in tasks:
if isinstance(task, list): # they've specified kwargs
start_task(task[0], **task[1]) # so pass those to start_task
if isinstance(task, list): # they've specified kwargs
start(task[0], **task[1]) # so pass those to start_task
else: # otherwise, just pass task_name
start_task(task)
start(task)

def start_task(task_name, **kwargs):
def start(task_name, **kwargs):
"""Start a given task in a new thread. Pass args to the task's run()
function."""
print "Starting task '{0}' in a new thread...".format(task_name)

try:
task = task_list[task_name]
task = _tasks[task_name]
except KeyError:
print ("Couldn't find task '{0}': wiki/tasks/{0}.py does not exist.").format(task_name)
print ("Couldn't find task '{0}': bot/tasks/{0}.py does not exist.").format(task_name)
return

task_thread = threading.Thread(target=lambda: task_wrapper(task, **kwargs))
task_thread = threading.Thread(target=lambda: _wrapper(task, **kwargs))
task_thread.name = "{0} ({1})".format(task_name, time.strftime("%b %d %H:%M:%S"))

# stop bot task threads automagically if the main bot stops
task_thread.daemon = True

task_thread.start()

def task_wrapper(task, **kwargs):
"""Wrapper for task classes: run the task and catch any errors."""
try:
task.run(**kwargs)
except:
print "Task '{0}' raised an exception and had to stop:".format(task.task_name)
traceback.print_exc()
else:
print "Task '{0}' finished without error.".format(task.task_name)

+ 50
- 37
bot/watcher.py View File

@@ -10,25 +10,30 @@ wiki bot tasks being started (listed in wiki/tasks/) or messages being sent to
channels in the IRC frontend.
"""

from core import config
from irc.classes import Connection, RC, BrokenSocketException
from irc import watcher_logic
import config
from classes import Connection, RC, BrokenSocketException
import watcher_logic as logic

frontend_conn = None

def get_connection():
"""Return a new Connection() instance with information about our server
connection, but don't actually connect yet."""
"""Return a new Connection() instance with connection information.

Don't actually connect yet.
"""
cf = config.irc["watcher"]
connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"],
cf["realname"])
return connection

def main(connection, f_conn=None):
"""Main loop for the Watcher IRC Bot component. get_connection() should
have already been called and the connection should have been started with
connection.connect(). Accept the frontend connection as well as an optional
parameter in order to send messages directly to frontend IRC channels."""
"""Main loop for the Watcher IRC Bot component.
get_connection() should have already been called and the connection should
have been started with connection.connect(). Accept the frontend connection
as well as an optional parameter in order to send messages directly to
frontend IRC channels.
"""
global frontend_conn
frontend_conn = f_conn
read_buffer = str()
@@ -43,34 +48,42 @@ def main(connection, f_conn=None):
read_buffer = lines.pop()

for line in lines:
line = line.strip().split()

if line[1] == "PRIVMSG":
chan = line[2]

# ignore messages originating from channels not in our list, to
# prevent someone PMing us false data
if chan not in config.irc["watcher"]["channels"]:
continue

msg = ' '.join(line[3:])[1:]
rc = RC(msg) # new RC object to store this event's data
rc.parse() # parse a message into pagenames, usernames, etc.
process(rc) # report to frontend channels or start tasks

if line[0] == "PING": # if we are pinged, pong back to the server
connection.send("PONG %s" % line[1])

# when we've finished starting up, join all watcher channels
if line[1] == "376":
for chan in config.irc["watcher"]["channels"]:
connection.join(chan)

def process(rc):
"""Process a message from IRC (technically, an RC object). The actual
processing is configurable, so we don't have that hard-coded here. We
simply call irc/watcher_logic.py's process() function and expect a list of
channels back, which we report the event data to."""
_process_message(line)

def _process_message(line):
"""Process a single message from IRC."""
line = line.strip().split()

if line[1] == "PRIVMSG":
chan = line[2]

# Ignore messages originating from channels not in our list, to prevent
# someone PMing us false data:
if chan not in config.irc["watcher"]["channels"]:
continue

msg = ' '.join(line[3:])[1:]
rc = RC(msg) # new RC object to store this event's data
rc.parse() # parse a message into pagenames, usernames, etc.
process_rc(rc) # report to frontend channels or start tasks

# If we are pinged, pong back to the server:
elif line[0] == "PING":
msg = " ".join(("PONG", line[1]))
connection.send(msg)

# When we've finished starting up, join all watcher channels:
elif line[1] == "376":
for chan in config.irc["watcher"]["channels"]:
connection.join(chan)

def process_rc(rc):
"""Process a recent change event from IRC (or, an RC object).

The actual processing is configurable, so we don't have that hard-coded
here. We simply call watcher_logic's process() function and expect a list
of channels back, which we report the event data to.
"""
chans = watcher_logic.process(rc)
if chans and frontend_conn:
pretty = rc.get_pretty()


+ 10
- 10
bot/watcher_logic.py View File

@@ -12,7 +12,7 @@ sense for this sort of thing... so...

import re

from wiki import task_manager as tasks
import tasks

afc_prefix = "wikipedia( talk)?:(wikiproject )?articles for creation"

@@ -39,8 +39,8 @@ def process(rc):
chans.update(("##earwigbot", "#wikipedia-en-afc"))
if r_page.search(page_name):
tasks.start_task("afc_statistics", action="process_edit", page=rc.page)
tasks.start_task("afc_copyvios", action="process_edit", page=rc.page)
tasks.start("afc_statistics", action="process_edit", page=rc.page)
tasks.start("afc_copyvios", action="process_edit", page=rc.page)
chans.add("#wikipedia-en-afc")
elif r_ffu.match(page_name):
@@ -50,22 +50,22 @@ def process(rc):
chans.add("#wikipedia-en-afc")
elif rc.flags == "move" and (r_move1.match(comment) or
r_move2.match(comment)):
r_move2.match(comment)):
p = r_moved_pages.findall(rc.comment)[0]
tasks.start_task("afc_statistics", action="process_move", pages=p)
tasks.start_task("afc_copyvios", action="process_move", pages=p)
tasks.start("afc_statistics", action="process_move", pages=p)
tasks.start("afc_copyvios", action="process_move", pages=p)
chans.add("#wikipedia-en-afc")
elif rc.flags == "delete" and r_delete.match(comment):
p = r_deleted_page.findall(rc.comment)[0]
tasks.start_task("afc_statistics", action="process_delete", page=p)
tasks.start_task("afc_copyvios", action="process_delete", page=p)
tasks.start("afc_statistics", action="process_delete", page=p)
tasks.start("afc_copyvios", action="process_delete", page=p)
chans.add("#wikipedia-en-afc")
elif rc.flags == "restore" and r_restore.match(comment):
p = r_restored_page.findall(rc.comment)[0]
tasks.start_task("afc_statistics", action="process_restore", page=p)
tasks.start_task("afc_copyvios", action="process_restore", page=p)
tasks.start("afc_statistics", action="process_restore", page=p)
tasks.start("afc_copyvios", action="process_restore", page=p)
chans.add("#wikipedia-en-afc")
elif rc.flags == "protect" and r_protect.match(comment):


+ 3
- 3
earwigbot.py View File

@@ -20,7 +20,7 @@ from os import path
from sys import executable
from time import sleep

from core.config import verify_config
from bot import config

__author__ = "Ben Kurtovic"
__copyright__ = "Copyright (C) 2009, 2010, 2011 by Ben Kurtovic"
@@ -28,12 +28,12 @@ __license__ = "MIT License"
__version__ = "0.1-dev"
__email__ = "ben.kurtovic@verizon.net"

bot_script = path.join(path.dirname(path.abspath(__file__)), "core", "main.py")
bot_script = path.join(path.dirname(path.abspath(__file__)), "bot", "main.py")

def main():
print "EarwigBot v{0}\n".format(__version__)

is_encrypted = verify_config()
is_encrypted = config.verify_config()
if is_encrypted: # passwords in the config file are encrypted
key = getpass("Enter key to unencrypt bot passwords: ")
else:


Loading…
Cancel
Save