| @@ -280,8 +280,7 @@ class Page(CopyvioMixIn): | |||
| self._assert_existence() | |||
| def _edit(self, params=None, text=None, summary=None, minor=None, bot=None, | |||
| force=None, section=None, captcha_id=None, captcha_word=None, | |||
| tries=0): | |||
| force=None, section=None, captcha_id=None, captcha_word=None): | |||
| """Edit the page! | |||
| If *params* is given, we'll use it as our API query parameters. | |||
| @@ -316,7 +315,7 @@ class Page(CopyvioMixIn): | |||
| except exceptions.APIError as error: | |||
| if not hasattr(error, "code"): | |||
| raise # We can only handle errors with a code attribute | |||
| result = self._handle_edit_errors(error, params, tries) | |||
| result = self._handle_edit_errors(error, params) | |||
| # If everything was successful, reset invalidated attributes: | |||
| if result["edit"]["result"] == "Success": | |||
| @@ -360,12 +359,12 @@ class Page(CopyvioMixIn): | |||
| return params | |||
| def _handle_edit_errors(self, error, params, tries): | |||
| def _handle_edit_errors(self, error, params, retry=True): | |||
| """If our edit fails due to some error, try to handle it. | |||
| We'll either raise an appropriate exception (for example, if the page | |||
| 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). | |||
| is protected), or we'll try to fix it (for example, if the token is | |||
| invalid, we'll try to get a new one). | |||
| """ | |||
| perms = ["noedit", "noedit-anon", "cantcreate", "cantcreate-anon", | |||
| "protectedtitle", "noimageredirect", "noimageredirect-anon", | |||
| @@ -378,6 +377,14 @@ class Page(CopyvioMixIn): | |||
| self._basetimestamp = None | |||
| self._exists = self.PAGE_UNKNOWN | |||
| raise exceptions.EditConflictError(error.info) | |||
| elif error.code == "badtoken" and retry: | |||
| params["token"] = self.site.get_token("edit") | |||
| try: | |||
| return self.site.api_query(**params) | |||
| except exceptions.APIError as error: | |||
| if not hasattr(error, "code"): | |||
| raise # We can only handle errors with a code attribute | |||
| result = self._handle_edit_errors(error, params, retry=False) | |||
| elif error.code in ["emptypage", "emptynewsection"]: | |||
| raise exceptions.NoContentError(error.info) | |||
| elif error.code == "contenttoobig": | |||
| @@ -26,7 +26,7 @@ from json import loads | |||
| from logging import getLogger, NullHandler | |||
| from os.path import expanduser | |||
| from StringIO import StringIO | |||
| from threading import Lock | |||
| from threading import RLock | |||
| from time import sleep, time | |||
| from urllib import quote_plus, unquote_plus | |||
| from urllib2 import build_opener, HTTPCookieProcessor, URLError | |||
| @@ -124,7 +124,7 @@ class Site(object): | |||
| self._wait_between_queries = wait_between_queries | |||
| self._max_retries = 6 | |||
| self._last_query_time = 0 | |||
| self._api_lock = Lock() | |||
| self._api_lock = RLock() | |||
| self._api_info_cache = {"maxlag": 0, "lastcheck": 0} | |||
| # Attributes used for SQL queries: | |||
| @@ -133,7 +133,7 @@ class Site(object): | |||
| else: | |||
| self._sql_data = {} | |||
| self._sql_conn = None | |||
| self._sql_lock = Lock() | |||
| self._sql_lock = RLock() | |||
| self._sql_info_cache = {"replag": 0, "lastcheck": 0, "usable": None} | |||
| # Attribute used in copyright violation checks (see CopyrightMixIn): | |||
| @@ -298,7 +298,7 @@ class Site(object): | |||
| # 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 | |||
| params["token"] = self.get_token(params["action"], False) | |||
| params["token"] = self.get_token(params["action"]) | |||
| 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." | |||
| @@ -762,27 +762,20 @@ class Site(object): | |||
| result = list(self.sql_query(query)) | |||
| return int(result[0][0]) | |||
| def get_token(self, action, lock=True): | |||
| def get_token(self, action): | |||
| """Return a token for a data-modifying API action. | |||
| *action* must be one of the types listed on | |||
| <https://www.mediawiki.org/wiki/API:Tokens>. If it's given as a union | |||
| of types separated by |, then the function will return a dictionary | |||
| of tokens instead of a single one. *lock* is for internal usage; | |||
| setting it to ``False`` prevents the query from acquiring the internal | |||
| API access lock. | |||
| of tokens instead of a single one. | |||
| Raises :py:exc:`~earwigbot.exceptions.PermissionsError` if we don't | |||
| have permissions for the requested action(s), or they are invalid. | |||
| Raises :py:exc:`~earwigbot.exceptions.APIError` if there was some other | |||
| API issue. | |||
| """ | |||
| if lock: | |||
| res = self.api_query(action="tokens", type=action) | |||
| else: | |||
| params = {"action": "tokens", "type": action} | |||
| res = self._api_query(params, ae_retry=False) | |||
| res = self.api_query(action="tokens", type=action) | |||
| if "warnings" in res and "tokens" in res["warnings"]: | |||
| raise exceptions.PermissionsError(res["warnings"]["tokens"]["*"]) | |||
| if "|" in action: | |||