* Site's api_query() is much smarter. It uses a custom urllib2 URL opener with cookie support and catches URLErrors, raising its own brand new exception (SiteAPIError) when something is wrong. * The opener now uses a custom User-Agent, which is a constant in wiki.tools.constants. * Site instances automatically login via _login(), which accepts a username and password (provided via config by get_site()) and uses two api_query()s and stores the login data as cookies in self._cookiejar. Login data is not preserved between bot restarts yet. Login errors, e.g. a bad password or username, raise the new LoginError. * Site's get_user()'s username argument is now optional. If left blank, will return the current logged-in user, provided by an API query. * Misc cleanup throughout.tags/v0.1^2
@@ -3,9 +3,17 @@ | |||
""" | |||
EarwigBot's Wiki Toolset: Constants | |||
This module defines some useful constants. | |||
This module defines some useful constants, such as default namespace IDs for | |||
easy lookup and our user agent. | |||
Import with `from wiki.tools.constants import *`. | |||
""" | |||
import platform | |||
# User agent when making API queries | |||
USER_AGENT = "EarwigBot/0.1-dev (Python/{0}; https://github.com/earwig/earwigbot)".format(platform.python_version()) | |||
# Default namespace IDs | |||
NS_MAIN = 0 | |||
NS_TALK = 1 | |||
@@ -13,6 +13,14 @@ class SiteNotFoundError(WikiToolsetError): | |||
"""A site matching the args given to get_site() could not be found in the | |||
config file.""" | |||
class SiteAPIError(WikiToolsetError): | |||
"""We couldn't connect to a site's API, perhaps because the server doesn't | |||
exist, our URL is wrong, or they're having temporary problems.""" | |||
class LoginError(WikiToolsetError): | |||
"""An error occured while trying to login. Perhaps the username/password is | |||
incorrect.""" | |||
class NamespaceNotFoundError(WikiToolsetError): | |||
"""A requested namespace name or namespace ID does not exist.""" | |||
@@ -67,10 +67,14 @@ def _get_site_object_from_dict(name, d): | |||
namespaces = d["namespaces"] | |||
except KeyError: | |||
namespaces = None | |||
try: | |||
login = (config.wiki["username"], config.wiki["password"]) | |||
except KeyError: | |||
login = (None, None) | |||
return Site(name=name, project=project, lang=lang, base_url=base_url, | |||
article_path=article_path, script_path=script_path, | |||
sql=(sql_server, sql_db), namespaces=namespaces) | |||
sql=(sql_server, sql_db), namespaces=namespaces, login=login) | |||
def get_site(name=None, project=None, lang=None): | |||
"""Returns a Site instance based on information from our config file. | |||
@@ -86,6 +90,10 @@ def get_site(name=None, project=None, lang=None): | |||
member of config.wiki["sites"], `s`, for which s["project"] == project and | |||
s["lang"] == lang. | |||
We will attempt to login to the site automatically | |||
using config.wiki["username"] and config.wiki["password"] if both are | |||
defined. | |||
Specifying a project without a lang or a lang without a project will raise | |||
TypeError. If all three args are specified, `name` will be first tried, | |||
then `project` and `lang`. If, with any number of args, a site cannot be | |||
@@ -96,7 +104,8 @@ def get_site(name=None, project=None, lang=None): | |||
_load_config() | |||
# someone specified a project without a lang (or a lang without a project)! | |||
if (project is None and lang is not None) or (project is not None and lang is None): | |||
if (project is None and lang is not None) or (project is not None and | |||
lang is None): | |||
e = "Keyword arguments 'lang' and 'project' must be specified together." | |||
raise TypeError(e) | |||
@@ -120,13 +129,14 @@ def get_site(name=None, project=None, lang=None): | |||
try: | |||
site = config.wiki["sites"][name] | |||
except KeyError: | |||
if project is None: # implies lang is None, i.e., only name was given | |||
if project is None: # implies lang is None, so only name was given | |||
e = "Site '{0}' not found in config.".format(name) | |||
raise SiteNotFoundError(e) | |||
for sitename, site in config.wiki["sites"].items(): | |||
if site["project"] == project and site["lang"] == lang: | |||
return _get_site_object_from_dict(sitename, site) | |||
e = "Neither site '{0}' nor site '{1}:{2}' found in config.".format(name, project, lang) | |||
e = "Neither site '{0}' nor site '{1}:{2}' found in config." | |||
e.format(name, project, lang) | |||
raise SiteNotFoundError(e) | |||
else: | |||
return _get_site_object_from_dict(name, site) | |||
@@ -1,12 +1,13 @@ | |||
# -*- coding: utf-8 -*- | |||
from cookielib import CookieJar | |||
from json import loads | |||
from urllib import urlencode | |||
from urllib2 import urlopen | |||
from urllib2 import build_opener, HTTPCookieProcessor, URLError | |||
from wiki.tools.category import Category | |||
from wiki.tools.constants import * | |||
from wiki.tools.exceptions import NamespaceNotFoundError | |||
from wiki.tools.exceptions import * | |||
from wiki.tools.page import Page | |||
from wiki.tools.user import User | |||
@@ -17,10 +18,12 @@ 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): | |||
namespaces=None, login=(None, None)): | |||
""" | |||
Docstring needed | |||
""" | |||
# attributes referring to site information, filled in by an API query | |||
# if they are missing (and an API url is available) | |||
self._name = name | |||
self._project = project | |||
self._lang = lang | |||
@@ -30,9 +33,45 @@ class Site(object): | |||
self._sql = sql | |||
self._namespaces = namespaces | |||
# get all of the above attributes that were not specified by the user | |||
# set up cookiejar and URL opener for making API queries | |||
self._cookiejar = CookieJar(cookie_file) | |||
self._opener = build_opener(HTTPCookieProcessor(self._cookiejar)) | |||
self._opener.addheaders = [('User-agent', USER_AGENT)] | |||
# use a username and password to login if they were provided | |||
if login[0] is not None and login[1] is not None: | |||
self._login(login[0], login[1]) | |||
# get all of the above attributes that were not specified as arguments | |||
self._load_attributes() | |||
def _login(self, name, password, token="", attempt=0): | |||
""" | |||
Docstring needed | |||
""" | |||
params = {"action": "login", "lgname": name, "lgpassword": password, | |||
"lgtoken": token} | |||
result = self.api_query(params) | |||
res = result["login"]["result"] | |||
if res == "Success": | |||
return | |||
elif res == "NeedToken" and attempt == 0: | |||
token = result["login"]["token"] | |||
return self._login(name, password, token, attempt=1) | |||
else: | |||
if res == "Illegal": | |||
e = "The provided username is illegal." | |||
elif res == "NotExists": | |||
e = "The provided username does not exist." | |||
elif res == "EmptyPass": | |||
e = "No password was given." | |||
elif res == "WrongPass" or res == "WrongPluginPass": | |||
e = "The given password is incorrect." | |||
else: | |||
e = "Couldn't login; server says '{0}'.".format(res) | |||
raise LoginError(e) | |||
def _load_attributes(self, force=False): | |||
""" | |||
Docstring needed | |||
@@ -103,10 +142,24 @@ class Site(object): | |||
Docstring needed | |||
""" | |||
url = ''.join((self._base_url, self._script_path, "/api.php")) | |||
params["format"] = "json" | |||
params["format"] = "json" # this is the only format we understand | |||
data = urlencode(params) | |||
result = urlopen(url, data).read() | |||
return loads(result) | |||
try: | |||
response = self._opener.open(url, data) | |||
except URLError as error: | |||
if hasattr(error, "reason"): | |||
e = "API query at {0} failed because {1}.".format(error.geturl, | |||
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) | |||
else: | |||
e = "API query failed." | |||
raise SiteAPIError(e) | |||
else: | |||
result = response.read() | |||
return loads(result) # parse as a JSON object | |||
def name(self): | |||
""" | |||
@@ -195,8 +248,13 @@ class Site(object): | |||
pagename = "{0}:{1}".format(prefix, catname) | |||
return Category(self, pagename) | |||
def get_user(self, username): | |||
def get_user(self, username=None): | |||
""" | |||
Docstring needed | |||
""" | |||
if username is None: | |||
params = {"action": "query", "meta": "userinfo"} | |||
result = self.api_query(params) | |||
username = result["query"]["userinfo"]["name"] | |||
return User(self, username) |