@@ -11,6 +11,7 @@ class Command(BaseCommand): | |||
def process(self, data): | |||
self.site = wiki.get_site() | |||
self.site._maxlag = None | |||
self.data = data | |||
if not data.args: | |||
@@ -27,6 +27,7 @@ class Command(BaseCommand): | |||
def process(self, data): | |||
self.site = wiki.get_site() | |||
self.site._maxlag = None | |||
if data.line[1] == "JOIN": | |||
notice = self.get_join_notice() | |||
@@ -42,6 +42,6 @@ class Command(BaseCommand): | |||
elif command == "VERSION": | |||
default = "EarwigBot - 0.1-dev - Python/$1 https://github.com/earwig/earwigbot" | |||
vers = config.metadata.get("ircVersion", default) | |||
vers = config.irc.get("version", default) | |||
vers = vers.replace("$1", platform.python_version()) | |||
self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) |
@@ -0,0 +1,38 @@ | |||
# -*- coding: utf-8 -*- | |||
from urllib import quote_plus | |||
from classes import BaseCommand | |||
import wiki | |||
class Command(BaseCommand): | |||
"""Return a user's edit count.""" | |||
name = "editcount" | |||
def check(self, data): | |||
commands = ["ec", "editcount"] | |||
if data.is_command and data.command in commands: | |||
return True | |||
return False | |||
def process(self, data): | |||
if not data.args: | |||
name = data.nick | |||
else: | |||
name = ' '.join(data.args) | |||
site = wiki.get_site() | |||
site._maxlag = None | |||
user = site.get_user(name) | |||
try: | |||
count = user.editcount() | |||
except wiki.UserNotFoundError: | |||
msg = "the user \x0302{0}\x0301 does not exist." | |||
self.connection.reply(data, msg.format(name)) | |||
return | |||
safe = quote_plus(user.name()) | |||
url = "http://toolserver.org/~soxred93/pcount/index.php?name={0}&lang=en&wiki=wikipedia" | |||
msg = "\x0302{0}\x0301 has {1} edits ({2})." | |||
self.connection.reply(data, msg.format(name, count, url.format(safe))) |
@@ -0,0 +1,31 @@ | |||
# -*- coding: utf-8 -*- | |||
import random | |||
from classes import BaseCommand | |||
class Command(BaseCommand): | |||
"""Praise people!""" | |||
name = "praise" | |||
def check(self, data): | |||
commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove", | |||
"groovedog"] | |||
return data.is_command and data.command in commands | |||
def process(self, data): | |||
if data.command == "earwig": | |||
msg = "\x02Earwig\x0F is the bestest Python programmer ever!" | |||
elif data.command in ["leonard", "leonard^bloom"]: | |||
msg = "\x02Leonard^Bloom\x0F is the biggest slacker ever!" | |||
elif data.command in ["groove", "groovedog"]: | |||
msg = "\x02GrooveDog\x0F is the bestest heh evar!" | |||
else: | |||
if not data.args: | |||
msg = "You use this command to praise certain people. Who they are is a secret." | |||
else: | |||
msg = "You're doing it wrong." | |||
self.connection.reply(data, msg) | |||
return | |||
self.connection.say(data.chan, msg) |
@@ -0,0 +1,63 @@ | |||
# -*- coding: utf-8 -*- | |||
import time | |||
from classes import BaseCommand | |||
import wiki | |||
class Command(BaseCommand): | |||
"""Return when a user registered.""" | |||
name = "registration" | |||
def check(self, data): | |||
commands = ["registration", "age"] | |||
if data.is_command and data.command in commands: | |||
return True | |||
return False | |||
def process(self, data): | |||
if not data.args: | |||
name = data.nick | |||
else: | |||
name = ' '.join(data.args) | |||
site = wiki.get_site() | |||
site._maxlag = None | |||
user = site.get_user(name) | |||
try: | |||
reg = user.registration() | |||
except wiki.UserNotFoundError: | |||
msg = "the user \x0302{0}\x0301 does not exist." | |||
self.connection.reply(data, msg.format(name)) | |||
return | |||
date = time.strftime("%b %d, %Y at %H:%M:%S UTC", reg) | |||
age = self.get_diff(time.mktime(reg), time.mktime(time.gmtime())) | |||
g = user.gender() | |||
if g == "male": | |||
gender = "He's" | |||
elif g == "female": | |||
gender = "She's" | |||
else: | |||
gender = "They're" | |||
msg = "\x0302{0}\x0301 registered on {1}. {2} {3} old." | |||
self.connection.reply(data, msg.format(name, date, gender, age)) | |||
def get_diff(self, t1, t2): | |||
parts = {"years": 31536000, "days": 86400, "hours": 3600, | |||
"minutes": 60, "seconds": 1} | |||
msg = [] | |||
order = sorted(parts.items(), key=lambda x: x[1], reverse=True) | |||
for key, value in order: | |||
num = 0 | |||
while t2 - t1 > value: | |||
t1 += value | |||
num += 1 | |||
if num or (not num and msg): | |||
msg.append(" ".join((str(num), key))) | |||
return ", ".join(msg) |
@@ -4,7 +4,7 @@ from classes import BaseCommand | |||
import wiki | |||
class Command(BaseCommand): | |||
"""Retrieve a list of rights for a given username.""" | |||
"""Retrieve a list of rights for a given name.""" | |||
name = "rights" | |||
def check(self, data): | |||
@@ -15,18 +15,19 @@ class Command(BaseCommand): | |||
def process(self, data): | |||
if not data.args: | |||
self.connection.reply(data, "who do you want me to look up?") | |||
return | |||
name = data.nick | |||
else: | |||
name = ' '.join(data.args) | |||
username = ' '.join(data.args) | |||
site = wiki.get_site() | |||
user = site.get_user(username) | |||
site._maxlag = None | |||
user = site.get_user(name) | |||
try: | |||
rights = user.groups() | |||
except wiki.UserNotFoundError: | |||
msg = "the user \x0302{0}\x0301 does not exist." | |||
self.connection.reply(data, msg.format(username)) | |||
self.connection.reply(data, msg.format(name)) | |||
return | |||
try: | |||
@@ -34,4 +35,4 @@ class Command(BaseCommand): | |||
except ValueError: | |||
pass | |||
msg = "the rights for \x0302{0}\x0301 are {1}." | |||
self.connection.reply(data, msg.format(username, ', '.join(rights))) | |||
self.connection.reply(data, msg.format(name, ', '.join(rights))) |
@@ -16,6 +16,16 @@ class Category(Page): | |||
members -- returns a list of titles in the category | |||
""" | |||
def __repr__(self): | |||
"""Returns the canonical string representation of the Category.""" | |||
res = ", ".join(("Category(title={0!r}", "follow_redirects={1!r}", | |||
"site={2!r})")) | |||
return res.format(self._title, self._follow_redirects, self._site) | |||
def __str__(self): | |||
"""Returns a nice string representation of the Category.""" | |||
return '<Category "{0}" of {1}>'.format(self.title(), str(self._site)) | |||
def members(self, limit=50): | |||
"""Returns a list of titles in the category. | |||
@@ -3,7 +3,7 @@ | |||
""" | |||
EarwigBot's Wiki Toolset: Exceptions | |||
This module contains all exceptions used by the wiki package. | |||
This module contains all exceptions used by the wiki package. There are a lot. | |||
""" | |||
class WikiToolsetError(Exception): | |||
@@ -22,11 +22,6 @@ class LoginError(WikiToolsetError): | |||
"""An error occured while trying to login. Perhaps the username/password is | |||
incorrect.""" | |||
class PermissionsError(WikiToolsetError): | |||
"""We tried to do something we don't have permission to, like a non-admin | |||
trying to delete a page, or trying to edit a page when no login information | |||
was provided.""" | |||
class NamespaceNotFoundError(WikiToolsetError): | |||
"""A requested namespace name or namespace ID does not exist.""" | |||
@@ -45,3 +40,27 @@ class RedirectError(WikiToolsetError): | |||
class UserNotFoundError(WikiToolsetError): | |||
"""Attempting to get certain information about a user that does not | |||
exist.""" | |||
class EditError(WikiToolsetError): | |||
"""We got some error while editing. Sometimes, a subclass of this exception | |||
will be used, like PermissionsError or EditConflictError.""" | |||
class PermissionsError(EditError): | |||
"""We tried to do something we don't have permission to, like a non-admin | |||
trying to delete a page, or trying to edit a page when no login information | |||
was provided.""" | |||
class EditConflictError(EditError): | |||
"""We've gotten an edit conflict or a (rarer) delete/recreate conflict.""" | |||
class NoContentError(EditError): | |||
"""We tried to create a page or new section with no content.""" | |||
class ContentTooBigError(EditError): | |||
"""The edit we tried to push exceeded the article size limit.""" | |||
class SpamDetectedError(EditError): | |||
"""The spam filter refused our edit.""" | |||
class FilteredError(EditError): | |||
"""The edit filter refused our edit.""" |
@@ -86,7 +86,9 @@ def _get_site_object_from_dict(name, d): | |||
namespaces = d.get("namespaces", {}) | |||
login = (config.wiki.get("username"), config.wiki.get("password")) | |||
cookiejar = _get_cookiejar() | |||
user_agent = config.metadata.get("userAgent") | |||
user_agent = config.wiki.get("userAgent") | |||
assert_edit = config.wiki.get("assert") | |||
maxlag = config.wiki.get("maxlag") | |||
if user_agent: | |||
user_agent = user_agent.replace("$1", platform.python_version()) | |||
@@ -102,7 +104,7 @@ def _get_site_object_from_dict(name, d): | |||
return Site(name=name, project=project, lang=lang, base_url=base_url, | |||
article_path=article_path, script_path=script_path, sql=sql, | |||
namespaces=namespaces, login=login, cookiejar=cookiejar, | |||
user_agent=user_agent) | |||
user_agent=user_agent, assert_edit=assert_edit, maxlag=maxlag) | |||
def get_site(name=None, project=None, lang=None): | |||
"""Returns a Site instance based on information from our config file. | |||
@@ -1,6 +1,8 @@ | |||
# -*- coding: utf-8 -*- | |||
from hashlib import md5 | |||
import re | |||
from time import gmtime, strftime | |||
from urllib import quote | |||
from wiki.exceptions import * | |||
@@ -25,7 +27,9 @@ class Page(object): | |||
is_redirect -- returns True if the page is a redirect, else False | |||
toggle_talk -- returns a content page's talk page, or vice versa | |||
get -- returns page content | |||
get_redirect_target -- if the page is a redirect, returns its destination | |||
get_redirect_target -- if the page is a redirect, returns its destination | |||
edit -- replaces the page's content or creates a new page | |||
add_section -- add a new section at the bottom of the page | |||
""" | |||
def __init__(self, site, title, follow_redirects=False): | |||
@@ -54,6 +58,11 @@ class Page(object): | |||
self._content = None | |||
self._creator = None | |||
# Attributes used for editing/deleting/protecting/etc: | |||
self._token = None | |||
self._basetimestamp = None | |||
self._starttimestamp = None | |||
# Try to determine the page's namespace using our site's namespace | |||
# converter: | |||
prefix = self._title.split(":", 1)[0] | |||
@@ -72,6 +81,16 @@ class Page(object): | |||
else: | |||
self._is_talkpage = self._namespace % 2 == 1 | |||
def __repr__(self): | |||
"""Returns the canonical string representation of the Page.""" | |||
res = ", ".join(("Page(title={0!r}", "follow_redirects={1!r}", | |||
"site={2!r})")) | |||
return res.format(self._title, self._follow_redirects, self._site) | |||
def __str__(self): | |||
"""Returns a nice string representation of the Page.""" | |||
return '<Page "{0}" of {1}>'.format(self.title(), str(self._site)) | |||
def _force_validity(self): | |||
"""Used to ensure that our page's title is valid. | |||
@@ -124,16 +143,16 @@ class Page(object): | |||
"""Loads various data from the API in a single query. | |||
Loads self._title, ._exists, ._is_redirect, ._pageid, ._fullurl, | |||
._protection, ._namespace, ._is_talkpage, ._creator, and ._lastrevid | |||
using the API. It will do a query of its own unless `result` is | |||
provided, in which case we'll pretend `result` is what the query | |||
returned. | |||
._protection, ._namespace, ._is_talkpage, ._creator, ._lastrevid, | |||
._token, and ._starttimestamp using the API. It will do a query of | |||
its own unless `result` is provided, in which case we'll pretend | |||
`result` is what the query returned. | |||
Assuming the API is sound, this should not raise any exceptions. | |||
""" | |||
if result is None: | |||
params = {"action": "query", "rvprop": "user", "rvdir": "newer", | |||
"prop": "info|revisions", "rvlimit": 1, | |||
params = {"action": "query", "rvprop": "user", "intoken": "edit", | |||
"prop": "info|revisions", "rvlimit": 1, "rvdir": "newer", | |||
"titles": self._title, "inprop": "protection|url"} | |||
result = self._site._api_query(params) | |||
@@ -168,6 +187,13 @@ class Page(object): | |||
self._fullurl = res["fullurl"] | |||
self._protection = res["protection"] | |||
try: | |||
self._token = res["edittoken"] | |||
except KeyError: | |||
pass | |||
else: | |||
self._starttimestamp = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) | |||
# We've determined the namespace and talkpage status in __init__() | |||
# based on the title, but now we can be sure: | |||
self._namespace = res["ns"] | |||
@@ -192,13 +218,13 @@ class Page(object): | |||
""" | |||
if result is None: | |||
params = {"action": "query", "prop": "revisions", "rvlimit": 1, | |||
"rvprop": "content", "titles": self._title} | |||
"rvprop": "content|timestamp", "titles": self._title} | |||
result = self._site._api_query(params) | |||
res = result["query"]["pages"].values()[0] | |||
try: | |||
content = res["revisions"][0]["*"] | |||
self._content = content | |||
self._content = res["revisions"][0]["*"] | |||
self._basetimestamp = res["revisions"][0]["timestamp"] | |||
except KeyError: | |||
# This can only happen if the page was deleted since we last called | |||
# self._load_attributes(). In that case, some of our attributes are | |||
@@ -206,6 +232,166 @@ class Page(object): | |||
self._load_attributes() | |||
self._force_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): | |||
"""Edit the page! | |||
If `params` is given, we'll use it as our API query parameters. | |||
Otherwise, we'll build params using the given kwargs via | |||
_build_edit_params(). | |||
We'll then try to do the API query, and catch any errors the API raises | |||
in _handle_edit_errors(). We'll then throw these back as subclasses of | |||
EditError. | |||
""" | |||
# Try to get our edit token, and die if we can't: | |||
if not self._token: | |||
self._load_attributes() | |||
if not self._token: | |||
e = "You don't have permission to edit this page." | |||
raise PermissionsError(e) | |||
# Weed out invalid pages before we get too far: | |||
self._force_validity() | |||
# Build our API query string: | |||
if not params: | |||
params = self._build_edit_params(text, summary, minor, bot, force, | |||
section, captcha_id, captcha_word) | |||
else: # Make sure we have the right token: | |||
params["token"] = self._token | |||
# Try the API query, catching most errors with our handler: | |||
try: | |||
result = self._site._api_query(params) | |||
except SiteAPIError 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) | |||
# If everything was successful, reset invalidated attributes: | |||
if result["edit"]["result"] == "Success": | |||
self._content = None | |||
self._basetimestamp = None | |||
self._exists = 0 | |||
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 EditError(result["edit"]) | |||
self._handle_assert_edit(assertion, params, tries) | |||
def _build_edit_params(self, text, summary, minor, bot, force, section, | |||
captcha_id, captcha_word): | |||
"""Given some keyword arguments, build an API edit query string.""" | |||
hashed = md5(text).hexdigest() # Checksum to ensure text is correct | |||
params = {"action": "edit", "title": self._title, "text": text, | |||
"token": self._token, "summary": summary, "md5": hashed} | |||
if section: | |||
params["section"] = section | |||
if captcha_id and captcha_word: | |||
params["captchaid"] = captcha_id | |||
params["captchaword"] = captcha_word | |||
if minor: | |||
params["minor"] = "true" | |||
else: | |||
params["notminor"] = "true" | |||
if bot: | |||
params["bot"] = "true" | |||
if not force: | |||
params["starttimestamp"] = self._starttimestamp | |||
if self._basetimestamp: | |||
params["basetimestamp"] = self._basetimestamp | |||
if self._exists == 2: | |||
# Page does not exist; don't edit if it already exists: | |||
params["createonly"] = "true" | |||
else: | |||
params["recreate"] = "true" | |||
return params | |||
def _handle_edit_errors(self, error, params, tries): | |||
"""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). | |||
""" | |||
if error.code in ["noedit", "cantcreate", "protectedtitle", | |||
"noimageredirect"]: | |||
raise PermissionsError(error.info) | |||
elif error.code in ["noedit-anon", "cantcreate-anon", | |||
"noimageredirect-anon"]: | |||
if not all(self._site._login_info): | |||
# Insufficient login info: | |||
raise 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 LoginError(e) | |||
elif error.code in ["editconflict", "pagedeleted", "articleexists"]: | |||
# These attributes are now invalidated: | |||
self._content = None | |||
self._basetimestamp = None | |||
self._exists = 0 | |||
raise EditConflictError(error.info) | |||
elif error.code in ["emptypage", "emptynewsection"]: | |||
raise NoContentError(error.info) | |||
elif error.code == "contenttoobig": | |||
raise ContentTooBigError(error.info) | |||
elif error.code == "spamdetected": | |||
raise SpamDetectedError(error.info) | |||
elif error.code == "filtered": | |||
raise FilteredError(error.info) | |||
raise 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 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 LoginError(e) | |||
elif assertion == "bot": | |||
e = "AssertEdit: bot assertion failed; we don't have a bot flag!" | |||
raise PermissionsError(e) | |||
# Unknown assertion, maybe "true", "false", or "exists": | |||
e = "AssertEdit: assertion '{0}' failed.".format(assertion) | |||
raise PermissionsError(e) | |||
def title(self, force=False): | |||
"""Returns the Page's title, or pagename. | |||
@@ -394,9 +580,9 @@ class Page(object): | |||
if force or self._exists == 0: | |||
# Kill two birds with one stone by doing an API query for both our | |||
# attributes and our page content: | |||
params = {"action": "query", "rvprop": "content", "rvlimit": 1, | |||
params = {"action": "query", "rvlimit": 1, "titles": self._title, | |||
"prop": "info|revisions", "inprop": "protection|url", | |||
"titles": self._title} | |||
"intoken": "edit", "rvprop": "content|timestamp"} | |||
result = self._site._api_query(params) | |||
self._load_attributes(result=result) | |||
self._force_existence() | |||
@@ -438,3 +624,32 @@ class Page(object): | |||
except IndexError: | |||
e = "The page does not appear to have a redirect target." | |||
raise RedirectError(e) | |||
def edit(self, text, summary, minor=False, bot=True, force=False): | |||
"""Replaces the page's content or creates a new page. | |||
`text` is the new page content, with `summary` as the edit summary. | |||
If `minor` is True, the edit will be marked as minor. If `bot` is true, | |||
the edit will be marked as a bot edit, but only if we actually have a | |||
bot flag. | |||
Use `force` to push the new content even if there's an edit conflict or | |||
the page was deleted/recreated between getting our edit token and | |||
editing our page. Be careful with this! | |||
""" | |||
self._edit(text=text, summary=summary, minor=minor, bot=bot, | |||
force=force) | |||
def add_section(self, text, title, minor=False, bot=True, force=False): | |||
"""Adds a new section to the bottom of the page. | |||
The arguments for this are the same as those for edit(), but instead of | |||
providing a summary, you provide a section title. | |||
Likewise, raised exceptions are the same as edit()'s. | |||
This should create the page if it does not already exist, with just the | |||
new section as content. | |||
""" | |||
self._edit(text=text, summary=title, minor=minor, bot=bot, force=force, | |||
section="new") |
@@ -5,6 +5,7 @@ from gzip import GzipFile | |||
from json import loads | |||
from re import escape as re_escape, match as re_match | |||
from StringIO import StringIO | |||
from time import sleep | |||
from urllib import unquote_plus, urlencode | |||
from urllib2 import build_opener, HTTPCookieProcessor, URLError | |||
from urlparse import urlparse | |||
@@ -41,7 +42,7 @@ class Site(object): | |||
def __init__(self, name=None, project=None, lang=None, base_url=None, | |||
article_path=None, script_path=None, sql=(None, None), | |||
namespaces=None, login=(None, None), cookiejar=None, | |||
user_agent=None): | |||
user_agent=None, assert_edit=None, maxlag=None): | |||
"""Constructor for new Site instances. | |||
This probably isn't necessary to call yourself unless you're building a | |||
@@ -69,6 +70,11 @@ class Site(object): | |||
self._sql = sql | |||
self._namespaces = namespaces | |||
# Attributes used when querying the API: | |||
self._assert_edit = assert_edit | |||
self._maxlag = maxlag | |||
self._max_retries = 5 | |||
# Set up cookiejar and URL opener for making API queries: | |||
if cookiejar is not None: | |||
self._cookiejar = cookiejar | |||
@@ -90,22 +96,50 @@ class Site(object): | |||
if logged_in_as is None or name != logged_in_as: | |||
self._login(login) | |||
def _api_query(self, params): | |||
def __repr__(self): | |||
"""Returns the canonical string representation of the Site.""" | |||
res = ", ".join(( | |||
"Site(name={_name!r}", "project={_project!r}", "lang={_lang!r}", | |||
"base_url={_base_url!r}", "article_path={_article_path!r}", | |||
"script_path={_script_path!r}", "assert_edit={_assert_edit!r}", | |||
"maxlag={_maxlag!r}", "sql={_sql!r}", "login={0}", | |||
"user_agent={2!r}", "cookiejar={1})" | |||
)) | |||
name, password = self._login_info | |||
login = "({0}, {1})".format(repr(name), "hidden" if password else None) | |||
cookies = self._cookiejar.__class__.__name__ | |||
try: | |||
cookies += "({0!r})".format(self._cookiejar.filename) | |||
except AttributeError: | |||
cookies += "()" | |||
agent = self._opener.addheaders[0][1] | |||
return res.format(login, cookies, agent, **self.__dict__) | |||
def __str__(self): | |||
"""Returns a nice string representation of the Site.""" | |||
res = "<Site {0} ({1}:{2}) at {3}>" | |||
return res.format(self.name(), self.project(), self.lang(), | |||
self.domain()) | |||
def _api_query(self, params, tries=0, wait=5): | |||
"""Do an API query with `params` as a dict of parameters. | |||
This will first attempt to construct an API url from self._base_url and | |||
self._script_path. We need both of these, or else we'll raise | |||
SiteAPIError. | |||
We'll encode the given params, adding format=json along the way, and | |||
make the request through self._opener, which has built-in cookie | |||
We'll encode the given params, adding format=json along the way, as | |||
well as &assert= and &maxlag= based on self._assert_edit and _maxlag. | |||
We make the request through self._opener, which has built-in cookie | |||
support via self._cookiejar, a User-Agent (wiki.constants.USER_AGENT), | |||
and Accept-Encoding set to "gzip". | |||
Assuming everything went well, we'll gunzip the data (if compressed), | |||
load it as a JSON object, and return it. | |||
If our request failed, we'll raise SiteAPIError with details. | |||
If our request failed for some reason, we'll raise SiteAPIError with | |||
details. If that reason was due to maxlag, we'll sleep for a bit and | |||
then repeat the query until we exceed self._max_retries. | |||
There's helpful MediaWiki API documentation at | |||
<http://www.mediawiki.org/wiki/API>. | |||
@@ -115,7 +149,13 @@ class Site(object): | |||
raise SiteAPIError(e) | |||
url = ''.join((self._base_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 | |||
params["assert"] = self._assert_edit | |||
if self._maxlag: # If requested, don't overload the servers | |||
params["maxlag"] = self._maxlag | |||
data = urlencode(params) | |||
print url, data # debug code | |||
@@ -124,21 +164,46 @@ class Site(object): | |||
response = self._opener.open(url, data) | |||
except URLError as error: | |||
if hasattr(error, "reason"): | |||
e = "API query at {0} failed because {1}." | |||
e = e.format(error.geturl, error.reason) | |||
e = "API query failed: {0}.".format(error.reason) | |||
elif hasattr(error, "code"): | |||
e = "API query at {0} failed; got an error code of {1}." | |||
e = e.format(error.geturl, error.code) | |||
e = "API query failed: got an error code of {0}." | |||
e = e.format(error.code) | |||
else: | |||
e = "API query failed." | |||
raise SiteAPIError(e) | |||
result = response.read() | |||
if response.headers.get("Content-Encoding") == "gzip": | |||
stream = StringIO(result) | |||
gzipper = GzipFile(fileobj=stream) | |||
result = gzipper.read() | |||
try: | |||
res = loads(result) # Parse as a JSON object | |||
except ValueError: | |||
e = "API query failed: JSON could not be decoded." | |||
raise SiteAPIError(e) | |||
try: | |||
code = res["error"]["code"] | |||
info = res["error"]["info"] | |||
except (TypeError, KeyError): | |||
return res | |||
if code == "maxlag": | |||
if tries >= self._max_retries: | |||
e = "Maximum number of retries reached ({0})." | |||
raise SiteAPIError(e.format(self._max_retries)) | |||
tries += 1 | |||
msg = 'Server says: "{0}". Retrying in {1} seconds ({2}/{3}).' | |||
print msg.format(info, wait, tries, self._max_retries) | |||
sleep(wait) | |||
return self._api_query(params, tries=tries, wait=wait*3) | |||
else: | |||
result = response.read() | |||
if response.headers.get("Content-Encoding") == "gzip": | |||
stream = StringIO(result) | |||
gzipper = GzipFile(fileobj=stream) | |||
result = gzipper.read() | |||
return loads(result) # Parse as a JSON object | |||
e = 'API query failed: got error "{0}"; server says: "{1}".' | |||
error = SiteAPIError(e.format(code, info)) | |||
error.code, error.info = code, info | |||
raise error | |||
def _load_attributes(self, force=False): | |||
"""Load data about our Site from the API. | |||
@@ -1,6 +1,6 @@ | |||
# -*- coding: utf-8 -*- | |||
from time import strptime | |||
from time import gmtime, strptime | |||
from wiki.constants import * | |||
from wiki.exceptions import UserNotFoundError | |||
@@ -45,6 +45,14 @@ class User(object): | |||
self._site = site | |||
self._name = name | |||
def __repr__(self): | |||
"""Returns the canonical string representation of the User.""" | |||
return "User(name={0!r}, site={1!r})".format(self._name, self._site) | |||
def __str__(self): | |||
"""Returns a nice string representation of the User.""" | |||
return '<User "{0}" of {1}>'.format(self.name(), str(self._site)) | |||
def _get_attribute(self, attr, force): | |||
"""Internally used to get an attribute by name. | |||
@@ -101,7 +109,12 @@ class User(object): | |||
self._editcount = res["editcount"] | |||
reg = res["registration"] | |||
self._registration = strptime(reg, "%Y-%m-%dT%H:%M:%SZ") | |||
try: | |||
self._registration = strptime(reg, "%Y-%m-%dT%H:%M:%SZ") | |||
except TypeError: | |||
# Sometimes the API doesn't give a date; the user's probably really | |||
# old. There's nothing else we can do! | |||
self._registration = gmtime(0) | |||
try: | |||
res["emailable"] | |||