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