* 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 | 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 | # Default namespace IDs | ||||
NS_MAIN = 0 | NS_MAIN = 0 | ||||
NS_TALK = 1 | 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 | """A site matching the args given to get_site() could not be found in the | ||||
config file.""" | 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): | class NamespaceNotFoundError(WikiToolsetError): | ||||
"""A requested namespace name or namespace ID does not exist.""" | """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"] | namespaces = d["namespaces"] | ||||
except KeyError: | except KeyError: | ||||
namespaces = None | 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, | return Site(name=name, project=project, lang=lang, base_url=base_url, | ||||
article_path=article_path, script_path=script_path, | 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): | def get_site(name=None, project=None, lang=None): | ||||
"""Returns a Site instance based on information from our config file. | """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 | member of config.wiki["sites"], `s`, for which s["project"] == project and | ||||
s["lang"] == lang. | 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 | 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, | 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 | 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() | _load_config() | ||||
# someone specified a project without a lang (or a lang without a project)! | # 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." | e = "Keyword arguments 'lang' and 'project' must be specified together." | ||||
raise TypeError(e) | raise TypeError(e) | ||||
@@ -120,13 +129,14 @@ def get_site(name=None, project=None, lang=None): | |||||
try: | try: | ||||
site = config.wiki["sites"][name] | site = config.wiki["sites"][name] | ||||
except KeyError: | 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) | e = "Site '{0}' not found in config.".format(name) | ||||
raise SiteNotFoundError(e) | raise SiteNotFoundError(e) | ||||
for sitename, site in config.wiki["sites"].items(): | for sitename, site in config.wiki["sites"].items(): | ||||
if site["project"] == project and site["lang"] == lang: | if site["project"] == project and site["lang"] == lang: | ||||
return _get_site_object_from_dict(sitename, site) | 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) | raise SiteNotFoundError(e) | ||||
else: | else: | ||||
return _get_site_object_from_dict(name, site) | return _get_site_object_from_dict(name, site) | ||||
@@ -1,12 +1,13 @@ | |||||
# -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
from cookielib import CookieJar | |||||
from json import loads | from json import loads | ||||
from urllib import urlencode | from urllib import urlencode | ||||
from urllib2 import urlopen | |||||
from urllib2 import build_opener, HTTPCookieProcessor, URLError | |||||
from wiki.tools.category import Category | from wiki.tools.category import Category | ||||
from wiki.tools.constants import * | 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.page import Page | ||||
from wiki.tools.user import User | 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, | def __init__(self, name=None, project=None, lang=None, base_url=None, | ||||
article_path=None, script_path=None, sql=(None, None), | article_path=None, script_path=None, sql=(None, None), | ||||
namespaces=None): | |||||
namespaces=None, login=(None, None)): | |||||
""" | """ | ||||
Docstring needed | 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._name = name | ||||
self._project = project | self._project = project | ||||
self._lang = lang | self._lang = lang | ||||
@@ -30,9 +33,45 @@ class Site(object): | |||||
self._sql = sql | self._sql = sql | ||||
self._namespaces = namespaces | 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() | 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): | def _load_attributes(self, force=False): | ||||
""" | """ | ||||
Docstring needed | Docstring needed | ||||
@@ -103,10 +142,24 @@ class Site(object): | |||||
Docstring needed | Docstring needed | ||||
""" | """ | ||||
url = ''.join((self._base_url, self._script_path, "/api.php")) | 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) | 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): | def name(self): | ||||
""" | """ | ||||
@@ -195,8 +248,13 @@ class Site(object): | |||||
pagename = "{0}:{1}".format(prefix, catname) | pagename = "{0}:{1}".format(prefix, catname) | ||||
return Category(self, pagename) | return Category(self, pagename) | ||||
def get_user(self, username): | |||||
def get_user(self, username=None): | |||||
""" | """ | ||||
Docstring needed | 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) | return User(self, username) |