Browse Source

Update AssertEdit behavior now that it's been merged into MW core.

Note: don't deploy until January 14.
tags/v0.2
Ben Kurtovic 11 years ago
parent
commit
08af882841
3 changed files with 47 additions and 100 deletions
  1. +14
    -13
      earwigbot/exceptions.py
  2. +7
    -79
      earwigbot/wiki/page.py
  3. +26
    -8
      earwigbot/wiki/site.py

+ 14
- 13
earwigbot/exceptions.py View File

@@ -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.




+ 7
- 79
earwigbot/wiki/page.py View File

@@ -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."""


+ 26
- 8
earwigbot/wiki/site.py View File

@@ -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))


Loading…
Cancel
Save