Note: don't deploy until January 14.tags/v0.2
@@ -36,13 +36,13 @@ This module contains all exceptions used by EarwigBot:: | |||||
| +-- SQLError | | +-- SQLError | ||||
+-- NoServiceError | +-- NoServiceError | ||||
+-- LoginError | +-- LoginError | ||||
+-- PermissionsError | |||||
+-- NamespaceNotFoundError | +-- NamespaceNotFoundError | ||||
+-- PageNotFoundError | +-- PageNotFoundError | ||||
+-- InvalidPageError | +-- InvalidPageError | ||||
+-- RedirectError | +-- RedirectError | ||||
+-- UserNotFoundError | +-- UserNotFoundError | ||||
+-- EditError | +-- EditError | ||||
| +-- PermissionsError | |||||
| +-- EditConflictError | | +-- EditConflictError | ||||
| +-- NoContentError | | +-- NoContentError | ||||
| +-- ContentTooBigError | | +-- ContentTooBigError | ||||
@@ -120,6 +120,19 @@ class LoginError(WikiToolsetError): | |||||
Raised by :py:meth:`Site._login <earwigbot.wiki.site.Site._login>`. | 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): | class NamespaceNotFoundError(WikiToolsetError): | ||||
"""A requested namespace name or namespace ID does not exist. | """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>`. | :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): | class EditConflictError(EditError): | ||||
"""We gotten an edit conflict or a (rarer) delete/recreate conflict. | """We gotten an edit conflict or a (rarer) delete/recreate conflict. | ||||
@@ -308,6 +308,7 @@ class Page(CopyvioMixIn): | |||||
section, captcha_id, captcha_word) | section, captcha_id, captcha_word) | ||||
else: # Make sure we have the right token: | else: # Make sure we have the right token: | ||||
params["token"] = self._token | params["token"] = self._token | ||||
self._token = None # Token now invalid | |||||
# Try the API query, catching most errors with our handler: | # Try the API query, catching most errors with our handler: | ||||
try: | try: | ||||
@@ -324,13 +325,8 @@ class Page(CopyvioMixIn): | |||||
self._exists = self.PAGE_UNKNOWN | self._exists = self.PAGE_UNKNOWN | ||||
return | 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, | def _build_edit_params(self, text, summary, minor, bot, force, section, | ||||
captcha_id, captcha_word): | 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 | 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). | 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) | 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"]: | elif error.code in ["editconflict", "pagedeleted", "articleexists"]: | ||||
# These attributes are now invalidated: | # These attributes are now invalidated: | ||||
self._content = None | self._content = None | ||||
self._basetimestamp = None | self._basetimestamp = None | ||||
self._exists = self.PAGE_UNKNOWN | self._exists = self.PAGE_UNKNOWN | ||||
raise exceptions.EditConflictError(error.info) | raise exceptions.EditConflictError(error.info) | ||||
elif error.code in ["emptypage", "emptynewsection"]: | elif error.code in ["emptypage", "emptynewsection"]: | ||||
raise exceptions.NoContentError(error.info) | 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": | elif error.code == "contenttoobig": | ||||
raise exceptions.ContentTooBigError(error.info) | raise exceptions.ContentTooBigError(error.info) | ||||
elif error.code == "spamdetected": | elif error.code == "spamdetected": | ||||
raise exceptions.SpamDetectedError(error.info) | raise exceptions.SpamDetectedError(error.info) | ||||
elif error.code == "filtered": | elif error.code == "filtered": | ||||
raise exceptions.FilteredError(error.info) | raise exceptions.FilteredError(error.info) | ||||
raise exceptions.EditError(": ".join((error.code, 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 | @property | ||||
def site(self): | def site(self): | ||||
"""The page's corresponding Site object.""" | """The page's corresponding Site object.""" | ||||
@@ -209,11 +209,13 @@ class Site(object): | |||||
args.append(key + "=" + val) | args.append(key + "=" + val) | ||||
return "&".join(args) | 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. | """Do an API query with *params* as a dict of parameters. | ||||
See the documentation for :py:meth:`api_query` for full implementation | 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 | since_last_query = time() - self._last_query_time # Throttling support | ||||
if since_last_query < self._wait_between_queries: | if since_last_query < self._wait_between_queries: | ||||
@@ -247,7 +249,7 @@ class Site(object): | |||||
gzipper = GzipFile(fileobj=stream) | gzipper = GzipFile(fileobj=stream) | ||||
result = gzipper.read() | 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): | def _build_api_query(self, params, ignore_maxlag): | ||||
"""Given API query params, return the URL to query and POST data.""" | """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")) | url = ''.join((self.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 | ||||
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 | params["assert"] = self._assert_edit | ||||
if self._maxlag and not ignore_maxlag: | if self._maxlag and not ignore_maxlag: | ||||
# If requested, don't overload the servers: | # If requested, don't overload the servers: | ||||
@@ -266,7 +269,7 @@ class Site(object): | |||||
data = self._urlencode_utf8(params) | data = self._urlencode_utf8(params) | ||||
return url, data | 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.""" | """Given the result of an API query, attempt to return useful data.""" | ||||
try: | try: | ||||
res = loads(result) # Try to parse as a JSON object | res = loads(result) # Try to parse as a JSON object | ||||
@@ -277,8 +280,8 @@ class Site(object): | |||||
try: | try: | ||||
code = res["error"]["code"] | code = res["error"]["code"] | ||||
info = res["error"]["info"] | 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 code == "maxlag": # We've been throttled by the server | ||||
if tries >= self._max_retries: | if tries >= self._max_retries: | ||||
@@ -288,7 +291,22 @@ class Site(object): | |||||
msg = 'Server says "{0}"; retrying in {1} seconds ({2}/{3})' | msg = 'Server says "{0}"; retrying in {1} seconds ({2}/{3})' | ||||
self._logger.info(msg.format(info, wait, tries, self._max_retries)) | self._logger.info(msg.format(info, wait, tries, self._max_retries)) | ||||
sleep(wait) | 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 | else: # Some unknown error occurred | ||||
e = 'API query failed: got error "{0}"; server says: "{1}".' | e = 'API query failed: got error "{0}"; server says: "{1}".' | ||||
error = exceptions.APIError(e.format(code, info)) | error = exceptions.APIError(e.format(code, info)) | ||||