Note: don't deploy until January 14.tags/v0.2
@@ -36,13 +36,13 @@ This module contains all exceptions used by EarwigBot:: | |||
| +-- SQLError | |||
+-- NoServiceError | |||
+-- LoginError | |||
+-- PermissionsError | |||
+-- NamespaceNotFoundError | |||
+-- PageNotFoundError | |||
+-- InvalidPageError | |||
+-- RedirectError | |||
+-- UserNotFoundError | |||
+-- EditError | |||
| +-- PermissionsError | |||
| +-- EditConflictError | |||
| +-- NoContentError | |||
| +-- ContentTooBigError | |||
@@ -120,6 +120,19 @@ class LoginError(WikiToolsetError): | |||
Raised by :py:meth:`Site._login <earwigbot.wiki.site.Site._login>`. | |||
""" | |||
class PermissionsError(WikiToolsetError): | |||
"""A permissions error ocurred. | |||
We tried to do something we don't have permission to, like trying to delete | |||
a page as a non-admin, or trying to edit a page without login information | |||
and AssertEdit enabled. This will also be raised if we have been blocked | |||
from editing. | |||
Raised by :py:meth:`Page.edit <earwigbot.wiki.page.Page.edit>`, | |||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`, and | |||
other API methods depending on settings. | |||
""" | |||
class NamespaceNotFoundError(WikiToolsetError): | |||
"""A requested namespace name or namespace ID does not exist. | |||
@@ -164,18 +177,6 @@ class EditError(WikiToolsetError): | |||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | |||
""" | |||
class PermissionsError(EditError): | |||
"""A permissions error ocurred while editing. | |||
We tried to do something we don't have permission to, like trying to delete | |||
a page as a non-admin, or trying to edit a page without login information | |||
and AssertEdit enabled. This will also be raised if we have been blocked | |||
from editing. | |||
Raised by :py:meth:`Page.edit <earwigbot.wiki.page.Page.edit>` and | |||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | |||
""" | |||
class EditConflictError(EditError): | |||
"""We gotten an edit conflict or a (rarer) delete/recreate conflict. | |||
@@ -308,6 +308,7 @@ class Page(CopyvioMixIn): | |||
section, captcha_id, captcha_word) | |||
else: # Make sure we have the right token: | |||
params["token"] = self._token | |||
self._token = None # Token now invalid | |||
# Try the API query, catching most errors with our handler: | |||
try: | |||
@@ -324,13 +325,8 @@ class Page(CopyvioMixIn): | |||
self._exists = self.PAGE_UNKNOWN | |||
return | |||
# If we're here, then the edit failed. If it's because of AssertEdit, | |||
# handle that. Otherwise, die - something odd is going on: | |||
try: | |||
assertion = result["edit"]["assert"] | |||
except KeyError: | |||
raise exceptions.EditError(result["edit"]) | |||
self._handle_assert_edit(assertion, params, tries) | |||
# Otherwise, there was some kind of problem. Throw an exception: | |||
raise exceptions.EditError(result["edit"]) | |||
def _build_edit_params(self, text, summary, minor, bot, force, section, | |||
captcha_id, captcha_word): | |||
@@ -371,95 +367,27 @@ class Page(CopyvioMixIn): | |||
is protected), or we'll try to fix it (for example, if we can't edit | |||
due to being logged out, we'll try to log in). | |||
""" | |||
if error.code in ["noedit", "cantcreate", "protectedtitle", | |||
"noimageredirect"]: | |||
perms = ["noedit", "noedit-anon", "cantcreate", "cantcreate-anon", | |||
"protectedtitle", "noimageredirect", "noimageredirect-anon", | |||
"blocked"] | |||
if error.code in perms: | |||
raise exceptions.PermissionsError(error.info) | |||
elif error.code in ["noedit-anon", "cantcreate-anon", | |||
"noimageredirect-anon"]: | |||
if not all(self.site._login_info): | |||
# Insufficient login info: | |||
raise exceptions.PermissionsError(error.info) | |||
if tries == 0: | |||
# We have login info; try to login: | |||
self.site._login(self.site._login_info) | |||
self._token = None # Need a new token; old one is invalid now | |||
return self._edit(params=params, tries=1) | |||
else: | |||
# We already tried to log in and failed! | |||
e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." | |||
raise exceptions.LoginError(e) | |||
elif error.code in ["editconflict", "pagedeleted", "articleexists"]: | |||
# These attributes are now invalidated: | |||
self._content = None | |||
self._basetimestamp = None | |||
self._exists = self.PAGE_UNKNOWN | |||
raise exceptions.EditConflictError(error.info) | |||
elif error.code in ["emptypage", "emptynewsection"]: | |||
raise exceptions.NoContentError(error.info) | |||
elif error.code == "blocked": | |||
if tries > 0 or not all(self.site._login_info): | |||
raise exceptions.PermissionsError(error.info) | |||
else: | |||
# Perhaps we are blocked from being logged-out? Try to log in: | |||
self.site._login(self.site._login_info) | |||
self._token = None # Need a new token; old one is invalid now | |||
return self._edit(params=params, tries=1) | |||
elif error.code == "contenttoobig": | |||
raise exceptions.ContentTooBigError(error.info) | |||
elif error.code == "spamdetected": | |||
raise exceptions.SpamDetectedError(error.info) | |||
elif error.code == "filtered": | |||
raise exceptions.FilteredError(error.info) | |||
raise exceptions.EditError(": ".join((error.code, error.info))) | |||
def _handle_assert_edit(self, assertion, params, tries): | |||
"""If we can't edit due to a failed AssertEdit assertion, handle that. | |||
If the assertion was 'user' and we have valid login information, try to | |||
log in. Otherwise, raise PermissionsError with details. | |||
""" | |||
if assertion == "user": | |||
if not all(self.site._login_info): | |||
# Insufficient login info: | |||
e = "AssertEdit: user assertion failed, and no login info was provided." | |||
raise exceptions.PermissionsError(e) | |||
if tries == 0: | |||
# We have login info; try to login: | |||
self.site._login(self.site._login_info) | |||
self._token = None # Need a new token; old one is invalid now | |||
return self._edit(params=params, tries=1) | |||
else: | |||
# We already tried to log in and failed! | |||
e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." | |||
raise exceptions.LoginError(e) | |||
elif assertion == "bot": | |||
if not all(self.site._login_info): | |||
# Insufficient login info: | |||
e = "AssertEdit: bot assertion failed, and no login info was provided." | |||
raise exceptions.PermissionsError(e) | |||
if tries == 0: | |||
# Try to log in if we got logged out: | |||
self.site._login(self.site._login_info) | |||
self._token = None # Need a new token; old one is invalid now | |||
return self._edit(params=params, tries=1) | |||
else: | |||
# We already tried to log in, so we don't have a bot flag: | |||
e = "AssertEdit: bot assertion failed: we don't have a bot flag!" | |||
raise exceptions.PermissionsError(e) | |||
# Unknown assertion, maybe "true", "false", or "exists": | |||
e = "AssertEdit: assertion '{0}' failed.".format(assertion) | |||
raise exceptions.PermissionsError(e) | |||
@property | |||
def site(self): | |||
"""The page's corresponding Site object.""" | |||
@@ -209,11 +209,13 @@ class Site(object): | |||
args.append(key + "=" + val) | |||
return "&".join(args) | |||
def _api_query(self, params, tries=0, wait=5, ignore_maxlag=False): | |||
def _api_query(self, params, tries=0, wait=5, ignore_maxlag=False, | |||
ae_retry=True): | |||
"""Do an API query with *params* as a dict of parameters. | |||
See the documentation for :py:meth:`api_query` for full implementation | |||
details. | |||
details. *tries*, *wait*, and *ignore_maxlag* are for maxlag; | |||
*ae_retry* is for AssertEdit. | |||
""" | |||
since_last_query = time() - self._last_query_time # Throttling support | |||
if since_last_query < self._wait_between_queries: | |||
@@ -247,7 +249,7 @@ class Site(object): | |||
gzipper = GzipFile(fileobj=stream) | |||
result = gzipper.read() | |||
return self._handle_api_query_result(result, params, tries, wait) | |||
return self._handle_api_result(result, params, tries, wait, ae_retry) | |||
def _build_api_query(self, params, ignore_maxlag): | |||
"""Given API query params, return the URL to query and POST data.""" | |||
@@ -257,7 +259,8 @@ class Site(object): | |||
url = ''.join((self.url, self._script_path, "/api.php")) | |||
params["format"] = "json" # This is the only format we understand | |||
if self._assert_edit: # If requested, ensure that we're logged in | |||
if self._assert_edit and params.get("action") != "login": | |||
# If requested, ensure that we're logged in | |||
params["assert"] = self._assert_edit | |||
if self._maxlag and not ignore_maxlag: | |||
# If requested, don't overload the servers: | |||
@@ -266,7 +269,7 @@ class Site(object): | |||
data = self._urlencode_utf8(params) | |||
return url, data | |||
def _handle_api_query_result(self, result, params, tries, wait): | |||
def _handle_api_result(self, result, params, tries, wait, ae_retry): | |||
"""Given the result of an API query, attempt to return useful data.""" | |||
try: | |||
res = loads(result) # Try to parse as a JSON object | |||
@@ -277,8 +280,8 @@ class Site(object): | |||
try: | |||
code = res["error"]["code"] | |||
info = res["error"]["info"] | |||
except (TypeError, KeyError): # Having these keys indicates a problem | |||
return res # All is well; return the decoded JSON | |||
except (TypeError, KeyError): # If there's no error code/info, return | |||
return res | |||
if code == "maxlag": # We've been throttled by the server | |||
if tries >= self._max_retries: | |||
@@ -288,7 +291,22 @@ class Site(object): | |||
msg = 'Server says "{0}"; retrying in {1} seconds ({2}/{3})' | |||
self._logger.info(msg.format(info, wait, tries, self._max_retries)) | |||
sleep(wait) | |||
return self._api_query(params, tries=tries, wait=wait*2) | |||
return self._api_query(params, tries, wait * 2, ae_retry=ae_retry) | |||
elif code in ["assertuserfailed", "assertbotfailed"]: # AssertEdit | |||
if ae_retry and all(self._login_info): | |||
# Try to log in if we got logged out: | |||
self._login(self._login_info) | |||
if "token" in params: # Fetch a new one; this is invalid now | |||
tparams = {"action": "tokens", "type": params["action"]} | |||
params["token"] = self._api_query(tparams, ae_retry=False) | |||
return self._api_query(params, tries, wait, ae_retry=False) | |||
if not all(self._login_info): | |||
e = "Assertion failed, and no login info was provided." | |||
elif code == "assertbotfailed": | |||
e = "Bot assertion failed: we don't have a bot flag!" | |||
else: | |||
e = "User assertion failed due to an unknown issue. Cookie problem?" | |||
raise exceptions.PermissionsError("AssertEdit: " + e) | |||
else: # Some unknown error occurred | |||
e = 'API query failed: got error "{0}"; server says: "{1}".' | |||
error = exceptions.APIError(e.format(code, info)) | |||