@@ -442,8 +442,8 @@ class Blowfish(object): | |||||
def encrypt(self, data): | def encrypt(self, data): | ||||
if not len(data) == 8: | 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 | # 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) | 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): | def decrypt(self, data): | ||||
if not len(data) == 8: | 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 | # 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) | 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 | return 56 * 8 | ||||
def verify_key(self, key): | 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: | if not key: | ||||
raise KeyLengthError("no key given") | raise KeyLengthError("no key given") | ||||
if len(key) < 8: | 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: | 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): | def encrypt(key, plaintext): | ||||
"""Encrypt any length of plaintext using a given key that must be between | """Encrypt any length of plaintext using a given key that must be between | ||||
@@ -518,24 +520,25 @@ def decrypt(key, cyphertext): | |||||
try: | try: | ||||
cyphertext = cyphertext.decode("hex") | cyphertext = cyphertext.decode("hex") | ||||
except TypeError as error: | |||||
except (TypeError, AttributeError) as error: | |||||
e = error.message | e = error.message | ||||
raise DecryptionError("cyphertext could not be decoded: " + e.lower()) | raise DecryptionError("cyphertext could not be decoded: " + e.lower()) | ||||
if len(cyphertext) % 8 > 0: | 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)] | blocks = [cyphertext[f:f+8] for f in range(0, len(cyphertext), 8)] | ||||
msg = ''.join(map(cypher.decrypt, blocks)) | 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) | size, msg = msg[4:].split("|", 1) | ||||
while len(msg) > int(size): | 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 | return msg | ||||
@@ -4,6 +4,7 @@ import platform | |||||
import time | import time | ||||
from classes import BaseCommand | from classes import BaseCommand | ||||
import config | |||||
class Command(BaseCommand): | class Command(BaseCommand): | ||||
"""Not an actual command, this module is used to respond to the CTCP | """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)) | self.connection.notice(target, "\x01TIME {0}\x01".format(ts)) | ||||
elif command == "VERSION": | 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)) | self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) |
@@ -8,13 +8,19 @@ including encrypting and decrypting passwords and making a new config file from | |||||
scratch at the inital bot run. | scratch at the inital bot run. | ||||
Usually you'll just want to do "from core import config" and access config data | 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 | import json | ||||
@@ -26,19 +32,12 @@ script_dir = path.dirname(path.abspath(__file__)) | |||||
root_dir = path.split(script_dir)[0] | root_dir = path.split(script_dir)[0] | ||||
config_path = path.join(root_dir, "config.json") | 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.""" | """Load data from our JSON config file (config.json) into _config.""" | ||||
global _config | global _config | ||||
with open(config_path, 'r') as fp: | with open(config_path, 'r') as fp: | ||||
@@ -49,86 +48,92 @@ def load_config(): | |||||
print error | print error | ||||
exit(1) | 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: | 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!" | print "You haven't configured the bot yet!" | ||||
choice = raw_input("Would you like to do this now? [y/n] ") | choice = raw_input("Would you like to do this now? [y/n] ") | ||||
if choice.lower().startswith("y"): | if choice.lower().startswith("y"): | ||||
return make_new_config() | |||||
return _make_new() | |||||
else: | else: | ||||
exit(1) | 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: | ||||
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: | except blowfish.BlowfishError as error: | ||||
print "\nError decrypting passwords:" | print "\nError decrypting passwords:" | ||||
print "{0}: {1}.".format(error.__class__.__name__, error) | print "{0}: {1}.".format(error.__class__.__name__, error) | ||||
exit(1) | 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): | 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, | now = {"minute": minute, "hour": hour, "month_day": month_day, | ||||
"month": month, "week_day": week_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: | for event in data: | ||||
do = True | do = True | ||||
for key, value in now.items(): | for key, value in now.items(): | ||||
@@ -146,15 +151,3 @@ def schedule(minute, hour, month_day, month, week_day): | |||||
pass | pass | ||||
return tasks | 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 |
@@ -4,9 +4,9 @@ | |||||
""" | """ | ||||
EarwigBot's Core | 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 | 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 | 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 | (irc, scheduler, etc) and making sure they are all happy. An explanation of the | ||||
@@ -103,12 +103,14 @@ def irc_frontend(): | |||||
f_conn.close() | f_conn.close() | ||||
def run(): | def run(): | ||||
config.load() | |||||
try: | 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 | enabled = config.components | ||||
if "irc_frontend" in enabled: # make the frontend run on our primary | 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 | tasks.load() # watcher on another thread iff it | ||||
if "irc_watcher" in enabled: # is enabled | if "irc_watcher" in enabled: # is enabled | ||||
print "\nStarting IRC watcher..." | 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.name = "irc-watcher" | ||||
t_watcher.daemon = True | t_watcher.daemon = True | ||||
t_watcher.start() | t_watcher.start() | ||||
@@ -3,18 +3,18 @@ | |||||
""" | """ | ||||
EarwigBot's Wiki Toolset: Constants | 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 | 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()) | 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_MAIN = 0 | ||||
NS_TALK = 1 | NS_TALK = 1 | ||||
NS_USER = 2 | NS_USER = 2 | ||||
@@ -14,6 +14,7 @@ from cookielib import LWPCookieJar, LoadError | |||||
import errno | import errno | ||||
from getpass import getpass | from getpass import getpass | ||||
from os import chmod, path | from os import chmod, path | ||||
import platform | |||||
import stat | import stat | ||||
import config | import config | ||||
@@ -30,12 +31,10 @@ def _load_config(): | |||||
directly from Python's interpreter and not the bot itself, because | directly from Python's interpreter and not the bot itself, because | ||||
earwigbot.py or core/main.py will already call these functions. | 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 | if is_encrypted: # passwords in the config file are encrypted | ||||
key = getpass("Enter key to unencrypt bot passwords: ") | key = getpass("Enter key to unencrypt bot passwords: ") | ||||
config.parse_config(key) | |||||
else: | |||||
config.parse_config(None) | |||||
config.decrypt(key) | |||||
def _get_cookiejar(): | def _get_cookiejar(): | ||||
"""Returns a LWPCookieJar object loaded from our .cookies file. The same | """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", {}) | namespaces = d.get("namespaces", {}) | ||||
login = (config.wiki.get("username"), config.wiki.get("password")) | login = (config.wiki.get("username"), config.wiki.get("password")) | ||||
cookiejar = _get_cookiejar() | 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 | for key, value in namespaces.items(): # Convert string keys to integers | ||||
del namespaces[key] | 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, | return Site(name=name, project=project, lang=lang, base_url=base_url, | ||||
article_path=article_path, script_path=script_path, sql=sql, | 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): | def get_site(name=None, project=None, lang=None): | ||||
"""Returns a Site instance based on information from our config file. | """Returns a Site instance based on information from our config file. | ||||
@@ -40,7 +40,8 @@ class Site(object): | |||||
def __init__(self, name=None, project=None, lang=None, base_url=None, | def __init__(self, name=None, project=None, lang=None, base_url=None, | ||||
article_path=None, script_path=None, sql=(None, 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. | """Constructor for new Site instances. | ||||
This probably isn't necessary to call yourself unless you're building a | 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 | the API, and then log in if a username/pass was given and we aren't | ||||
already logged in. | 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._name = name | ||||
self._project = project | self._project = project | ||||
self._lang = lang | self._lang = lang | ||||
@@ -68,19 +69,21 @@ class Site(object): | |||||
self._sql = sql | self._sql = sql | ||||
self._namespaces = namespaces | 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: | if cookiejar is not None: | ||||
self._cookiejar = cookiejar | self._cookiejar = cookiejar | ||||
else: | else: | ||||
self._cookiejar = CookieJar() | 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 = build_opener(HTTPCookieProcessor(self._cookiejar)) | ||||
self._opener.addheaders = [("User-Agent", USER_AGENT), | |||||
self._opener.addheaders = [("User-Agent", user_agent), | |||||
("Accept-Encoding", "gzip")] | ("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() | 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 | self._login_info = name, password = login | ||||
if name is not None and password is not None: | if name is not None and password is not None: | ||||
logged_in_as = self._get_username_from_cookies() | logged_in_as = self._get_username_from_cookies() | ||||
@@ -112,7 +115,7 @@ class Site(object): | |||||
raise SiteAPIError(e) | raise SiteAPIError(e) | ||||
url = ''.join((self._base_url, self._script_path, "/api.php")) | 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) | data = urlencode(params) | ||||
print url, data # debug code | print url, data # debug code | ||||
@@ -135,7 +138,7 @@ class Site(object): | |||||
stream = StringIO(result) | stream = StringIO(result) | ||||
gzipper = GzipFile(fileobj=stream) | gzipper = GzipFile(fileobj=stream) | ||||
result = gzipper.read() | 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): | def _load_attributes(self, force=False): | ||||
"""Load data about our Site from the API. | """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 | Additionally, you can call this with `force=True` to forcibly reload | ||||
all attributes. | 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, | attrs = [self._name, self._project, self._lang, self._base_url, | ||||
self._article_path, self._script_path] | self._article_path, self._script_path] | ||||
@@ -158,9 +161,9 @@ class Site(object): | |||||
params["siprop"] = "general|namespaces|namespacealiases" | params["siprop"] = "general|namespaces|namespacealiases" | ||||
result = self._api_query(params) | result = self._api_query(params) | ||||
self._load_namespaces(result) | 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 | 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" | params["siprop"] = "general" | ||||
result = self._api_query(params) | result = self._api_query(params) | ||||
@@ -240,9 +243,9 @@ class Site(object): | |||||
continue | continue | ||||
if cookie.name != name: | if cookie.name != name: | ||||
continue | 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))) | 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) | user_name = self._get_cookie("centralauth_User", cookie.domain) | ||||
if user_name is not None: | if user_name is not None: | ||||
return user_name.value | return user_name.value | ||||
@@ -402,7 +405,7 @@ class Site(object): | |||||
""" | """ | ||||
lname = name.lower() | lname = name.lower() | ||||
for ns_id, names in self._namespaces.items(): | 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: | if lname in lnames: | ||||
return ns_id | return ns_id | ||||
@@ -421,7 +424,7 @@ class Site(object): | |||||
""" | """ | ||||
prefixes = self.namespace_id_to_name(NS_CATEGORY, all=True) | prefixes = self.namespace_id_to_name(NS_CATEGORY, all=True) | ||||
prefix = title.split(":", 1)[0] | 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: | if prefix in prefixes: | ||||
return Category(self, title, follow_redirects) | return Category(self, title, follow_redirects) | ||||
return Page(self, title, follow_redirects) | return Page(self, title, follow_redirects) | ||||
@@ -33,7 +33,7 @@ bot_script = path.join(path.dirname(path.abspath(__file__)), "bot", "main.py") | |||||
def main(): | def main(): | ||||
print "EarwigBot v{0}\n".format(__version__) | 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 | if is_encrypted: # passwords in the config file are encrypted | ||||
key = getpass("Enter key to unencrypt bot passwords: ") | key = getpass("Enter key to unencrypt bot passwords: ") | ||||
else: | else: | ||||
@@ -41,7 +41,7 @@ def main(): | |||||
while 1: | while 1: | ||||
bot = Popen([executable, bot_script], stdin=PIPE) | 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() | return_code = bot.wait() | ||||
if return_code == 1: | if return_code == 1: | ||||
exit() # let critical exceptions in the subprocess cause us to | exit() # let critical exceptions in the subprocess cause us to | ||||