Kaynağa Gözat

Reorganizing functions in config.py; store User-Agent and IRC version in config; exception, docstring, and comment cleanup.

tags/v0.1^2
Ben Kurtovic 13 yıl önce
ebeveyn
işleme
868ad8eab8
8 değiştirilmiş dosya ile 154 ekleme ve 147 silme
  1. +20
    -17
      bot/blowfish.py
  2. +4
    -2
      bot/commands/ctcp.py
  3. +83
    -90
      bot/config.py
  4. +10
    -8
      bot/main.py
  5. +6
    -6
      bot/wiki/constants.py
  6. +9
    -5
      bot/wiki/functions.py
  7. +20
    -17
      bot/wiki/site.py
  8. +2
    -2
      earwigbot.py

+ 20
- 17
bot/blowfish.py Dosyayı Görüntüle

@@ -442,8 +442,8 @@ class Blowfish(object):

def encrypt(self, data):
if not len(data) == 8:
raise BlockSizeError("blocks must be 8 bytes long, but tried to " +
"encrypt one {0} bytes long".format(len(data)))
e = "blocks must be 8 bytes long, but tried to encrypt one {0} bytes long"
raise BlockSizeError(e.format(len(data)))

# Use big endianess since that's what everyone else uses
xl = ord (data[3]) | (ord (data[2]) << 8) | (ord (data[1]) << 16) | (ord (data[0]) << 24)
@@ -458,8 +458,8 @@ class Blowfish(object):

def decrypt(self, data):
if not len(data) == 8:
raise BlockSizeError("blocks must be 8 bytes long, but tried to " +
"decrypt one {0} bytes long".format(len(data)))
e = "blocks must be 8 bytes long, but tried to decrypt one {0} bytes long"
raise BlockSizeError(e.format(len(data)))

# Use big endianess since that's what everyone else uses
cl = ord (data[3]) | (ord (data[2]) << 8) | (ord (data[1]) << 16) | (ord (data[0]) << 24)
@@ -482,16 +482,18 @@ class Blowfish(object):
return 56 * 8

def verify_key(self, key):
"""Make sure our key is not too short or too long; if there's a
problem, raise KeyTooShortError() or KeyTooLongError()."""
"""Make sure our key is not too short or too long.
If there's a problem, raise KeyTooShortError() or KeyTooLongError().
"""
if not key:
raise KeyLengthError("no key given")
if len(key) < 8:
raise KeyLengthError(("key is {0} bytes long, but it must be at " +
"least 8").format(len(key)))
e = "key is {0} bytes long, but it must be at least 8"
raise KeyLengthError(e.format(len(key)))
if len(key) > 56:
raise KeyLengthError(("key is {0} bytes long, but it must be " +
"less than 56").format(len(key)))
e = "key is {0} bytes long, but it must be less than 56"
raise KeyLengthError(e.format(len(key)))

def encrypt(key, plaintext):
"""Encrypt any length of plaintext using a given key that must be between
@@ -518,24 +520,25 @@ def decrypt(key, cyphertext):

try:
cyphertext = cyphertext.decode("hex")
except TypeError as error:
except (TypeError, AttributeError) as error:
e = error.message
raise DecryptionError("cyphertext could not be decoded: " + e.lower())

if len(cyphertext) % 8 > 0:
raise DecryptionError("cyphertext cannot be broken into " +
"8-byte blocks evenly")
e = "cyphertext cannot be broken into 8-byte blocks evenly"
raise DecryptionError(e)

blocks = [cyphertext[f:f+8] for f in range(0, len(cyphertext), 8)]
msg = ''.join(map(cypher.decrypt, blocks))

if not msg.startswith("TRUE"): # sanity check to ensure valid decryption
raise DecryptionError("the given key is incorrect, or part of the " +
"cyphertext is malformed")
# Sanity check to ensure valid decryption:
if not msg.startswith("TRUE"):
e = "the given key is incorrect, or part of the cyphertext is malformed"
raise DecryptionError(e)

size, msg = msg[4:].split("|", 1)
while len(msg) > int(size):
msg = msg[:-1] # remove the padding that we applied earlier
msg = msg[:-1] # Remove the padding that we applied earlier

return msg



+ 4
- 2
bot/commands/ctcp.py Dosyayı Görüntüle

@@ -4,6 +4,7 @@ import platform
import time

from classes import BaseCommand
import config

class Command(BaseCommand):
"""Not an actual command, this module is used to respond to the CTCP
@@ -40,6 +41,7 @@ class Command(BaseCommand):
self.connection.notice(target, "\x01TIME {0}\x01".format(ts))

elif command == "VERSION":
vers = "EarwigBot - 0.1-dev - Python/{0} https://github.com/earwig/earwigbot"
vers = vers.format(platform.python_version())
default = "EarwigBot - 0.1-dev - Python/$1 https://github.com/earwig/earwigbot"
vers = config.metadata.get("ircVersion", default)
vers = vers.replace("$1", platform.python_version())
self.connection.notice(target, "\x01VERSION {0}\x01".format(vers))

+ 83
- 90
bot/config.py Dosyayı Görüntüle

@@ -8,13 +8,19 @@ 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 core import config" and access config data
from within config's three global variables and one function:

* config.components - a list of enabled components
* config.wiki - a dict of config information for wiki-editing
* config.irc - a dict of config information for IRC
* config.schedule() - returns a list of tasks scheduled to run at a given
time
from within config's four global variables and one function:

* config.components - a list of enabled components
* config.wiki - a dict of information about wiki-editing
* config.irc - a dict of information about IRC
* config.metadata - a dict of miscellaneous information
* config.schedule() - returns a list of tasks scheduled to run at a given time

Additionally, functions related to 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
"""

import json
@@ -26,19 +32,12 @@ script_dir = path.dirname(path.abspath(__file__))
root_dir = path.split(script_dir)[0]
config_path = path.join(root_dir, "config.json")

_config = None # holds data loaded from our config file

# set our three easy-config-access global variables to None
components, wiki, irc = (None, None, None)
_config = None # Holds data loaded from our config file

def is_config_loaded():
"""Return True if our config file has already been loaded, and False if it
hasn't."""
if _config is not None:
return True
return False
# Set our four easy-config-access global variables to None
components, wiki, irc, metadata = None, None, None, None

def load_config():
def _load():
"""Load data from our JSON config file (config.json) into _config."""
global _config
with open(config_path, 'r') as fp:
@@ -49,86 +48,92 @@ def load_config():
print error
exit(1)

def verify_config():
"""Check to see if we have a valid config file, and if not, notify the
user. If there is no config file at all, offer to make one; otherwise,
exit. If everything goes well, return True if stored passwords are
encrypted in the file, or False if they are not."""
if path.exists(config_path):
load_config()
try:
return _config["encryptPasswords"] # are passwords encrypted?
except KeyError:
return False # assume passwords are not encrypted by default
def _make_new():
"""Make a new config file based on the user's input."""
encrypt = raw_input("Would you like to encrypt passwords stored in config.json? [y/n] ")
if encrypt.lower().startswith("y"):
is_encrypted = True
else:
is_encrypted = False

return is_encrypted

def is_loaded():
"""Return True if our config file has been loaded, otherwise False."""
return _config is not None

def load():
"""Load, or reload, our config file.

First, check if we have a valid config file, and if not, notify the user.
If there is no config file at all, offer to make one, otherwise exit.

Store data from our config file in four global variables (components, wiki,
irc, metadata) for easy access (as well as the internal _config variable).

If everything goes well, return True if stored passwords are
encrypted in the file, or False if they are not.
"""
global components, wiki, irc, metadata

if not path.exists(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 make_new_config()
return _make_new()
else:
exit(1)

def parse_config(key):
"""Store data from our config file in three global variables for easy
access, and use the key to unencrypt passwords. Catch password decryption
errors and report them to the user."""
global components, wiki, irc
_load()

load_config() # we might be re-loading unnecessarily here, but no harm in
# that!
try:
components = _config["components"]
except KeyError:
components = []
try:
wiki = _config["wiki"]
except KeyError:
wiki = {}
try:
irc = _config["irc"]
except KeyError:
irc = {}
components = _config.get("components", [])
wiki = _config.get("wiki", {})
irc = _config.get("irc", {})
metadata = _config.get("metadata", {})

# Are passwords encrypted?
return metadata.get("encryptPasswords", False)

def decrypt(key):
"""Use the key to decrypt passwords in our config file.
Call this if load() returns True. Catch password decryption errors and
report them to the user.
"""
global irc, wiki

try:
try:
if _config["encryptPasswords"]:
decrypt(key, "wiki['password']")
decrypt(key, "irc['frontend']['nickservPassword']")
decrypt(key, "irc['watcher']['nickservPassword']")
except KeyError:
pass
item = wiki.get("password")
if item:
wiki["password"] = blowfish.decrypt(key, item)

item = irc.get("frontend").get("nickservPassword")
if item:
irc["frontend"]["nickservPassword"] = blowfish.decrypt(key, item)

item = irc.get("watcher").get("nickservPassword")
if item:
irc["watcher"]["nickservPassword"] = blowfish.decrypt(key, item)

except blowfish.BlowfishError as error:
print "\nError decrypting passwords:"
print "{0}: {1}.".format(error.__class__.__name__, error)
exit(1)

def decrypt(key, item):
"""Decrypt 'item' with blowfish.decrypt() using the given key and set it to
the decrypted result. 'item' should be a string, like
decrypt(key, "wiki['password']"), NOT decrypt(key, wiki['password'),
because that won't work."""
global irc, wiki
try:
result = blowfish.decrypt(key, eval(item))
except KeyError:
return
exec "{0} = result".format(item)

def schedule(minute, hour, month_day, month, week_day):
"""Return a list of tasks that are scheduled to run at the time specified
by the function arguments. The schedule data comes from our config file's
'schedule' field, which is stored as _config["schedule"]. Call this
function with config.schedule(args)."""
tasks = [] # tasks to run this turn, each as a tuple of either (task_name,
# kwargs), or just task_name
"""Return a list of tasks scheduled to run at the specified time.

The schedule data comes from our config file's 'schedule' field, which is
stored as _config["schedule"]. Call this function as config.schedule(args).
"""
# Tasks to run this turn, each as a list of either [task_name, kwargs], or
# just the task_name:
tasks = []

now = {"minute": minute, "hour": hour, "month_day": month_day,
"month": month, "week_day": week_day}

try:
data = _config["schedule"]
except KeyError:
return [] # nothing is in our schedule
data = _config.get("schedule", [])
for event in data:
do = True
for key, value in now.items():
@@ -146,15 +151,3 @@ def schedule(minute, hour, month_day, month, week_day):
pass

return tasks

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

encrypt = raw_input("Would you like to encrypt passwords stored in " +
"config.json? [y/n] ")
if encrypt.lower().startswith("y"):
is_encrypted = True
else:
is_encrypted = False

return is_encrypted

+ 10
- 8
bot/main.py Dosyayı Görüntüle

@@ -4,9 +4,9 @@
"""
EarwigBot's Core

This (should) not be run directly; the wrapper in "earwigbot.py" is preferred,
This should not be run directly; the wrapper in "earwigbot.py" is preferred,
but it should work fine alone, as long as you enter the password-unlock key at
the initial hidden prompt.
the initial hidden prompt if one is needed.

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
@@ -103,12 +103,14 @@ def irc_frontend():
f_conn.close()

def run():
config.load()
try:
key = raw_input() # wait for our password unlock key from the bot's
except EOFError: # wrapper
key = None
config.parse_config(key) # load data from the config file and parse it
# using the unlock key
key = raw_input() # wait for our password decrypt key from the bot's
except EOFError: # wrapper, then decrypt passwords
pass
else:
config.decrypt(key)

enabled = config.components

if "irc_frontend" in enabled: # make the frontend run on our primary
@@ -120,7 +122,7 @@ def run():
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=())
t_watcher = threading.Thread(target=irc_watcher)
t_watcher.name = "irc-watcher"
t_watcher.daemon = True
t_watcher.start()


+ 6
- 6
bot/wiki/constants.py Dosyayı Görüntüle

@@ -3,18 +3,18 @@
"""
EarwigBot's Wiki Toolset: Constants

This module defines some useful constants, such as default namespace IDs for
easy lookup and our user agent.
This module defines some useful constants:
* USER_AGENT - our default User Agent when making API queries
* NS_* - default namespace IDs for easy lookup

Import with `from wiki.constants import *`.
Import with `from wiki import constants` or `from wiki.constants import *`.
"""

# Default User Agent when making API queries:
import platform

# User agent when making API queries
USER_AGENT = "EarwigBot/0.1-dev (Python/{0}; https://github.com/earwig/earwigbot)".format(platform.python_version())

# Default namespace IDs
# Default namespace IDs:
NS_MAIN = 0
NS_TALK = 1
NS_USER = 2


+ 9
- 5
bot/wiki/functions.py Dosyayı Görüntüle

@@ -14,6 +14,7 @@ from cookielib import LWPCookieJar, LoadError
import errno
from getpass import getpass
from os import chmod, path
import platform
import stat

import config
@@ -30,12 +31,10 @@ def _load_config():
directly from Python's interpreter and not the bot itself, because
earwigbot.py or core/main.py will already call these functions.
"""
is_encrypted = config.verify_config()
is_encrypted = config.load()
if is_encrypted: # passwords in the config file are encrypted
key = getpass("Enter key to unencrypt bot passwords: ")
config.parse_config(key)
else:
config.parse_config(None)
config.decrypt(key)

def _get_cookiejar():
"""Returns a LWPCookieJar object loaded from our .cookies file. The same
@@ -87,6 +86,10 @@ def _get_site_object_from_dict(name, d):
namespaces = d.get("namespaces", {})
login = (config.wiki.get("username"), config.wiki.get("password"))
cookiejar = _get_cookiejar()
user_agent = config.metadata.get("userAgent")

if user_agent:
user_agent = user_agent.replace("$1", platform.python_version())

for key, value in namespaces.items(): # Convert string keys to integers
del namespaces[key]
@@ -98,7 +101,8 @@ def _get_site_object_from_dict(name, d):

return Site(name=name, project=project, lang=lang, base_url=base_url,
article_path=article_path, script_path=script_path, sql=sql,
namespaces=namespaces, login=login, cookiejar=cookiejar)
namespaces=namespaces, login=login, cookiejar=cookiejar,
user_agent=user_agent)

def get_site(name=None, project=None, lang=None):
"""Returns a Site instance based on information from our config file.


+ 20
- 17
bot/wiki/site.py Dosyayı Görüntüle

@@ -40,7 +40,8 @@ class Site(object):

def __init__(self, name=None, project=None, lang=None, base_url=None,
article_path=None, script_path=None, sql=(None, None),
namespaces=None, login=(None, None), cookiejar=None):
namespaces=None, login=(None, None), cookiejar=None,
user_agent=None):
"""Constructor for new Site instances.

This probably isn't necessary to call yourself unless you're building a
@@ -57,8 +58,8 @@ class Site(object):
the API, and then log in if a username/pass was given and we aren't
already logged in.
"""
# attributes referring to site information, filled in by an API query
# if they are missing (and an API url can be determined)
# Attributes referring to site information, filled in by an API query
# if they are missing (and an API url can be determined):
self._name = name
self._project = project
self._lang = lang
@@ -68,19 +69,21 @@ class Site(object):
self._sql = sql
self._namespaces = namespaces

# set up cookiejar and URL opener for making API queries
# Set up cookiejar and URL opener for making API queries:
if cookiejar is not None:
self._cookiejar = cookiejar
else:
self._cookiejar = CookieJar()
if user_agent is None:
user_agent = USER_AGENT # Set default UA from wiki.constants
self._opener = build_opener(HTTPCookieProcessor(self._cookiejar))
self._opener.addheaders = [("User-Agent", USER_AGENT),
self._opener.addheaders = [("User-Agent", user_agent),
("Accept-Encoding", "gzip")]

# get all of the above attributes that were not specified as arguments
# Get all of the above attributes that were not specified as arguments:
self._load_attributes()

# if we have a name/pass and the API says we're not logged in, log in
# If we have a name/pass and the API says we're not logged in, log in:
self._login_info = name, password = login
if name is not None and password is not None:
logged_in_as = self._get_username_from_cookies()
@@ -112,7 +115,7 @@ class Site(object):
raise SiteAPIError(e)

url = ''.join((self._base_url, self._script_path, "/api.php"))
params["format"] = "json" # this is the only format we understand
params["format"] = "json" # This is the only format we understand
data = urlencode(params)

print url, data # debug code
@@ -135,7 +138,7 @@ class Site(object):
stream = StringIO(result)
gzipper = GzipFile(fileobj=stream)
result = gzipper.read()
return loads(result) # parse as a JSON object
return loads(result) # Parse as a JSON object

def _load_attributes(self, force=False):
"""Load data about our Site from the API.
@@ -147,8 +150,8 @@ class Site(object):
Additionally, you can call this with `force=True` to forcibly reload
all attributes.
"""
# all attributes to be loaded, except _namespaces, which is a special
# case because it requires additional params in the API query
# All attributes to be loaded, except _namespaces, which is a special
# case because it requires additional params in the API query:
attrs = [self._name, self._project, self._lang, self._base_url,
self._article_path, self._script_path]

@@ -158,9 +161,9 @@ class Site(object):
params["siprop"] = "general|namespaces|namespacealiases"
result = self._api_query(params)
self._load_namespaces(result)
elif all(attrs): # everything is already specified and we're not told
elif all(attrs): # Everything is already specified and we're not told
return # to force a reload, so do nothing
else: # we're only loading attributes other than _namespaces
else: # We're only loading attributes other than _namespaces
params["siprop"] = "general"
result = self._api_query(params)

@@ -240,9 +243,9 @@ class Site(object):
continue
if cookie.name != name:
continue
# build a regex that will match domains this cookie affects
# Build a regex that will match domains this cookie affects:
search = ''.join(("(.*?)", re_escape(cookie.domain)))
if re_match(search, domain): # test it against our site
if re_match(search, domain): # Test it against our site
user_name = self._get_cookie("centralauth_User", cookie.domain)
if user_name is not None:
return user_name.value
@@ -402,7 +405,7 @@ class Site(object):
"""
lname = name.lower()
for ns_id, names in self._namespaces.items():
lnames = [n.lower() for n in names] # be case-insensitive
lnames = [n.lower() for n in names] # Be case-insensitive
if lname in lnames:
return ns_id

@@ -421,7 +424,7 @@ class Site(object):
"""
prefixes = self.namespace_id_to_name(NS_CATEGORY, all=True)
prefix = title.split(":", 1)[0]
if prefix != title: # avoid a page that is simply "Category"
if prefix != title: # Avoid a page that is simply "Category"
if prefix in prefixes:
return Category(self, title, follow_redirects)
return Page(self, title, follow_redirects)


+ 2
- 2
earwigbot.py Dosyayı Görüntüle

@@ -33,7 +33,7 @@ bot_script = path.join(path.dirname(path.abspath(__file__)), "bot", "main.py")
def main():
print "EarwigBot v{0}\n".format(__version__)

is_encrypted = config.verify_config()
is_encrypted = config.load()
if is_encrypted: # passwords in the config file are encrypted
key = getpass("Enter key to unencrypt bot passwords: ")
else:
@@ -41,7 +41,7 @@ def main():

while 1:
bot = Popen([executable, bot_script], stdin=PIPE)
bot.communicate(key) # give the key to core.config.load_config()
bot.communicate(key) # give the key to core.config.decrypt()
return_code = bot.wait()
if return_code == 1:
exit() # let critical exceptions in the subprocess cause us to


Yükleniyor…
İptal
Kaydet