]> git.openstreetmap.org Git - osqa.git/blob - akismet.py
OSQA - 19
[osqa.git] / akismet.py
1 # Version 0.2.0
2 # 2009/06/18
3
4 # Copyright Michael Foord 2005-2009
5 # akismet.py
6 # Python interface to the akismet API
7 # E-mail fuzzyman@voidspace.org.uk
8
9 # http://www.voidspace.org.uk/python/modules.shtml
10 # http://akismet.com
11
12 # Released subject to the BSD License
13 # See http://www.voidspace.org.uk/python/license.shtml
14
15
16 """
17 A python interface to the `Akismet <http://akismet.com>`_ API.
18 This is a web service for blocking SPAM comments to blogs - or other online 
19 services.
20
21 You will need a Wordpress API key, from `wordpress.com <http://wordpress.com>`_.
22
23 You should pass in the keyword argument 'agent' to the name of your program,
24 when you create an Akismet instance. This sets the ``user-agent`` to a useful
25 value.
26
27 The default is : ::
28
29     Python Interface by Fuzzyman | akismet.py/0.2.0
30
31 Whatever you pass in, will replace the *Python Interface by Fuzzyman* part.
32 **0.2.0** will change with the version of this interface.
33
34 Usage example::
35     
36     from akismet import Akismet
37     
38     api = Akismet(agent='Test Script')
39     # if apikey.txt is in place,
40     # the key will automatically be set
41     # or you can call api.setAPIKey()
42     #
43     if api.key is None:
44         print "No 'apikey.txt' file."
45     elif not api.verify_key():
46         print "The API key is invalid."
47     else:
48         # data should be a dictionary of values
49         # They can all be filled in with defaults
50         # from a CGI environment
51         if api.comment_check(comment, data):
52             print 'This comment is spam.'
53         else:
54             print 'This comment is ham.'
55 """
56
57
58 import os, sys
59 from urllib import urlencode
60 from django.conf import settings
61 from forum import settings
62
63 import socket
64 if hasattr(socket, 'setdefaulttimeout'):
65     # Set the default timeout on sockets to 5 seconds
66     socket.setdefaulttimeout(5)
67
68 __version__ = '0.2.0'
69
70 __all__ = (
71     '__version__',
72     'Akismet',
73     'AkismetError',
74     'APIKeyError',
75     )
76
77 __author__ = 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>'
78
79 __docformat__ = "restructuredtext en"
80
81 user_agent = "%s | akismet.py/%s"
82 DEFAULTAGENT = 'Python Interface by Fuzzyman/%s'
83
84 isfile = os.path.isfile
85
86 urllib2 = None
87 try:
88     from google.appengine.api import urlfetch
89 except ImportError:
90     import urllib2
91
92 if urllib2 is None:
93     def _fetch_url(url, data, headers):
94         req = urlfetch.fetch(url=url, payload=data, method=urlfetch.POST, headers=headers)
95         if req.status_code == 200:
96             return req.content
97         raise Exception('Could not fetch Akismet URL: %s Response code: %s' % 
98                         (url, req.status_code))
99 else:
100     def _fetch_url(url, data, headers):
101         req = urllib2.Request(url, data, headers)
102         h = urllib2.urlopen(req)
103         resp = h.read()
104         return resp
105
106
107 class AkismetError(Exception):
108     """Base class for all akismet exceptions."""
109
110 class APIKeyError(AkismetError):
111     """Invalid API key."""
112
113 class Akismet(object):
114     """A class for working with the akismet API"""
115
116     baseurl = 'rest.akismet.com/1.1/'
117
118     def __init__(self, key=None, blog_url=None, agent=None):
119         """Automatically calls ``setAPIKey``."""
120         if agent is None:
121             agent = DEFAULTAGENT % __version__
122         self.user_agent = user_agent % (agent, __version__)
123         self.key = settings.WORDPRESS_API_KEY
124         self.blog_url = settings.WORDPRESS_BLOG_URL
125         # self.setAPIKey(key, blog_url)
126
127
128     def _getURL(self):
129         """
130         Fetch the url to make requests to.
131         
132         This comprises of api key plus the baseurl.
133         """
134         return 'http://%s.%s' % (self.key, self.baseurl)
135     
136     
137     def _safeRequest(self, url, data, headers):
138         try:
139             resp = _fetch_url(url, data, headers)
140         except Exception, e:
141             raise AkismetError(str(e))
142         return resp
143
144
145     def setAPIKey(self, key=None, blog_url=None):
146         """
147         Set the wordpress API key for all transactions.
148
149         If you don't specify an explicit API ``key`` and ``blog_url`` it will
150         attempt to load them from a file called ``apikey.txt`` in the current
151         directory.
152
153         This method is *usually* called automatically when you create a new
154         ``Akismet`` instance.
155         """
156         if key is None and isfile('apikey.txt'):
157             the_file = [l.strip() for l in open('apikey.txt').readlines()
158                 if l.strip() and not l.strip().startswith('#')]
159             try:
160                 self.key = the_file[0]
161                 self.blog_url = the_file[1]
162             except IndexError:
163                 raise APIKeyError("Your 'apikey.txt' is invalid.")
164         else:
165             self.key = settings.WORDPRESS_API_KEY
166             self.blog_url = blog_url
167
168
169     def verify_key(self):
170         """
171         This equates to the ``verify-key`` call against the akismet API.
172         
173         It returns ``True`` if the key is valid.
174         
175         The docs state that you *ought* to call this at the start of the
176         transaction.
177         
178         It raises ``APIKeyError`` if you have not yet set an API key.
179         
180         If the connection to akismet fails, it allows the normal ``HTTPError``
181         or ``URLError`` to be raised.
182         (*akismet.py* uses `urllib2 <http://docs.python.org/lib/module-urllib2.html>`_)
183         """
184         if self.key is None:
185             raise APIKeyError("Your have not set an API key.")
186         data = { 'key': self.key, 'blog': self.blog_url }
187         # this function *doesn't* use the key as part of the URL
188         url = 'http://%sverify-key' % self.baseurl
189         # we *don't* trap the error here
190         # so if akismet is down it will raise an HTTPError or URLError
191         headers = {'User-Agent' : self.user_agent}
192         resp = self._safeRequest(url, urlencode(data), headers)
193         if resp.lower() == 'valid':
194             return True
195         else:
196             return False
197
198     def _build_data(self, comment, data):
199         """
200         This function builds the data structure required by ``comment_check``,
201         ``submit_spam``, and ``submit_ham``.
202         
203         It modifies the ``data`` dictionary you give it in place. (and so
204         doesn't return anything)
205         
206         It raises an ``AkismetError`` if the user IP or user-agent can't be
207         worked out.
208         """
209         data['comment_content'] = comment
210         if not 'user_ip' in data:
211             try:
212                 val = os.environ['REMOTE_ADDR']
213             except KeyError:
214                 raise AkismetError("No 'user_ip' supplied")
215             data['user_ip'] = val
216         if not 'user_agent' in data:
217             try:
218                 val = os.environ['HTTP_USER_AGENT']
219             except KeyError:
220                 raise AkismetError("No 'user_agent' supplied")
221             data['user_agent'] = val
222         #
223         data.setdefault('referrer', os.environ.get('HTTP_REFERER', 'unknown'))
224         data.setdefault('permalink', '')
225         data.setdefault('comment_type', 'comment')
226         data.setdefault('comment_author', '')
227         data.setdefault('comment_author_email', '')
228         data.setdefault('comment_author_url', '')
229         data.setdefault('SERVER_ADDR', os.environ.get('SERVER_ADDR', ''))
230         data.setdefault('SERVER_ADMIN', os.environ.get('SERVER_ADMIN', ''))
231         data.setdefault('SERVER_NAME', os.environ.get('SERVER_NAME', ''))
232         data.setdefault('SERVER_PORT', os.environ.get('SERVER_PORT', ''))
233         data.setdefault('SERVER_SIGNATURE', os.environ.get('SERVER_SIGNATURE',
234             ''))
235         data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
236             ''))
237         data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
238         data.setdefault('blog', self.blog_url)
239
240
241     def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
242         """
243         This is the function that checks comments.
244         
245         It returns ``True`` for spam and ``False`` for ham.
246         
247         If you set ``DEBUG=True`` then it will return the text of the response,
248         instead of the ``True`` or ``False`` object.
249         
250         It raises ``APIKeyError`` if you have not yet set an API key.
251         
252         If the connection to Akismet fails then the ``HTTPError`` or
253         ``URLError`` will be propogated.
254         
255         As a minimum it requires the body of the comment. This is the
256         ``comment`` argument.
257         
258         Akismet requires some other arguments, and allows some optional ones.
259         The more information you give it, the more likely it is to be able to
260         make an accurate diagnosise.
261         
262         You supply these values using a mapping object (dictionary) as the
263         ``data`` argument.
264         
265         If ``build_data`` is ``True`` (the default), then *akismet.py* will
266         attempt to fill in as much information as possible, using default
267         values where necessary. This is particularly useful for programs
268         running in a {acro;CGI} environment. A lot of useful information
269         can be supplied from evironment variables (``os.environ``). See below.
270         
271         You *only* need supply values for which you don't want defaults filled
272         in for. All values must be strings.
273         
274         There are a few required values. If they are not supplied, and
275         defaults can't be worked out, then an ``AkismetError`` is raised.
276         
277         If you set ``build_data=False`` and a required value is missing an
278         ``AkismetError`` will also be raised.
279         
280         The normal values (and defaults) are as follows : ::
281         
282             'user_ip':          os.environ['REMOTE_ADDR']       (*)
283             'user_agent':       os.environ['HTTP_USER_AGENT']   (*)
284             'referrer':         os.environ.get('HTTP_REFERER', 'unknown') [#]_
285             'permalink':        ''
286             'comment_type':     'comment' [#]_
287             'comment_author':   ''
288             'comment_author_email': ''
289             'comment_author_url': ''
290             'SERVER_ADDR':      os.environ.get('SERVER_ADDR', '')
291             'SERVER_ADMIN':     os.environ.get('SERVER_ADMIN', '')
292             'SERVER_NAME':      os.environ.get('SERVER_NAME', '')
293             'SERVER_PORT':      os.environ.get('SERVER_PORT', '')
294             'SERVER_SIGNATURE': os.environ.get('SERVER_SIGNATURE', '')
295             'SERVER_SOFTWARE':  os.environ.get('SERVER_SOFTWARE', '')
296             'HTTP_ACCEPT':      os.environ.get('HTTP_ACCEPT', '')
297         
298         (*) Required values
299         
300         You may supply as many additional 'HTTP_*' type values as you wish.
301         These should correspond to the http headers sent with the request.
302         
303         .. [#] Note the spelling "referrer". This is a required value by the
304             akismet api - however, referrer information is not always
305             supplied by the browser or server. In fact the HTTP protocol
306             forbids relying on referrer information for functionality in 
307             programs.
308         .. [#] The `API docs <http://akismet.com/development/api/>`_ state that this value
309             can be " *blank, comment, trackback, pingback, or a made up value*
310             *like 'registration'* ".
311         """
312         if self.key is None:
313             raise APIKeyError("Your have not set an API key.")
314         if data is None:
315             data = {}
316         if build_data:
317             self._build_data(comment, data)
318         if 'blog' not in data:
319             data['blog'] = self.blog_url
320         url = '%scomment-check' % self._getURL()
321         # we *don't* trap the error here
322         # so if akismet is down it will raise an HTTPError or URLError
323         headers = {'User-Agent' : self.user_agent}
324         resp = self._safeRequest(url, urlencode(data), headers)
325         if DEBUG:
326             return resp
327         resp = resp.lower()
328         if resp == 'true':
329             return True
330         elif resp == 'false':
331             return False
332         else:
333             # NOTE: Happens when you get a 'howdy wilbur' response !
334             raise AkismetError('missing required argument.')
335
336
337     def submit_spam(self, comment, data=None, build_data=True):
338         """
339         This function is used to tell akismet that a comment it marked as ham,
340         is really spam.
341         
342         It takes all the same arguments as ``comment_check``, except for
343         *DEBUG*.
344         """
345         if self.key is None:
346             raise APIKeyError("Your have not set an API key.")
347         if data is None:
348             data = {}
349         if build_data:
350             self._build_data(comment, data)
351         url = '%ssubmit-spam' % self._getURL()
352         # we *don't* trap the error here
353         # so if akismet is down it will raise an HTTPError or URLError
354         headers = {'User-Agent' : self.user_agent}
355         self._safeRequest(url, urlencode(data), headers)
356
357
358     def submit_ham(self, comment, data=None, build_data=True):
359         """
360         This function is used to tell akismet that a comment it marked as spam,
361         is really ham.
362         
363         It takes all the same arguments as ``comment_check``, except for
364         *DEBUG*.
365         """
366         if self.key is None:
367             raise APIKeyError("Your have not set an API key.")
368         if data is None:
369             data = {}
370         if build_data:
371             self._build_data(comment, data)
372         url = '%ssubmit-ham' % self._getURL()
373         # we *don't* trap the error here
374         # so if akismet is down it will raise an HTTPError or URLError
375         headers = {'User-Agent' : self.user_agent}
376         self._safeRequest(url, urlencode(data), headers)