]> git.openstreetmap.org Git - osqa.git/blob - akismet.py
c330fb1c780b7a85d63dc861b5f625df57d42cec
[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
126
127     def _getURL(self):
128         """
129         Fetch the url to make requests to.
130         
131         This comprises of api key plus the baseurl.
132         """
133         return 'http://%s.%s' % (self.key, self.baseurl)
134     
135     
136     def _safeRequest(self, url, data, headers):
137         try:
138             resp = _fetch_url(url, data, headers)
139         except Exception, e:
140             raise AkismetError(str(e))
141         return resp
142
143
144     def setAPIKey(self, key=None, blog_url=None):
145         """
146         Set the wordpress API key for all transactions.
147
148         If you don't specify an explicit API ``key`` and ``blog_url`` it will
149         attempt to load them from a file called ``apikey.txt`` in the current
150         directory.
151
152         This method is *usually* called automatically when you create a new
153         ``Akismet`` instance.
154         """
155         if key is None and isfile('apikey.txt'):
156             the_file = [l.strip() for l in open('apikey.txt').readlines()
157                 if l.strip() and not l.strip().startswith('#')]
158             try:
159                 self.key = the_file[0]
160                 self.blog_url = the_file[1]
161             except IndexError:
162                 raise APIKeyError("Your 'apikey.txt' is invalid.")
163         else:
164             self.key = settings.WORDPRESS_API_KEY
165             self.blog_url = blog_url
166
167
168     def verify_key(self):
169         """
170         This equates to the ``verify-key`` call against the akismet API.
171         
172         It returns ``True`` if the key is valid.
173         
174         The docs state that you *ought* to call this at the start of the
175         transaction.
176         
177         It raises ``APIKeyError`` if you have not yet set an API key.
178         
179         If the connection to akismet fails, it allows the normal ``HTTPError``
180         or ``URLError`` to be raised.
181         (*akismet.py* uses `urllib2 <http://docs.python.org/lib/module-urllib2.html>`_)
182         """
183         if self.key is None:
184             raise APIKeyError("Your have not set an API key.")
185         data = { 'key': self.key, 'blog': self.blog_url }
186         # this function *doesn't* use the key as part of the URL
187         url = 'http://%sverify-key' % self.baseurl
188         # we *don't* trap the error here
189         # so if akismet is down it will raise an HTTPError or URLError
190         headers = {'User-Agent' : self.user_agent}
191         resp = self._safeRequest(url, urlencode(data), headers)
192         if resp.lower() == 'valid':
193             return True
194         else:
195             return False
196
197     def _build_data(self, comment, data):
198         """
199         This function builds the data structure required by ``comment_check``,
200         ``submit_spam``, and ``submit_ham``.
201         
202         It modifies the ``data`` dictionary you give it in place. (and so
203         doesn't return anything)
204         
205         It raises an ``AkismetError`` if the user IP or user-agent can't be
206         worked out.
207         """
208         data['comment_content'] = comment
209         if not 'user_ip' in data:
210             try:
211                 val = os.environ['REMOTE_ADDR']
212             except KeyError:
213                 raise AkismetError("No 'user_ip' supplied")
214             data['user_ip'] = val
215         if not 'user_agent' in data:
216             try:
217                 val = os.environ['HTTP_USER_AGENT']
218             except KeyError:
219                 raise AkismetError("No 'user_agent' supplied")
220             data['user_agent'] = val
221         #
222         data.setdefault('referrer', os.environ.get('HTTP_REFERER', 'unknown'))
223         data.setdefault('permalink', '')
224         data.setdefault('comment_type', 'comment')
225         data.setdefault('comment_author', '')
226         data.setdefault('comment_author_email', '')
227         data.setdefault('comment_author_url', '')
228         data.setdefault('SERVER_ADDR', os.environ.get('SERVER_ADDR', ''))
229         data.setdefault('SERVER_ADMIN', os.environ.get('SERVER_ADMIN', ''))
230         data.setdefault('SERVER_NAME', os.environ.get('SERVER_NAME', ''))
231         data.setdefault('SERVER_PORT', os.environ.get('SERVER_PORT', ''))
232         data.setdefault('SERVER_SIGNATURE', os.environ.get('SERVER_SIGNATURE',
233             ''))
234         data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
235             ''))
236         data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
237         data.setdefault('blog', self.blog_url)
238
239
240     def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
241         """
242         This is the function that checks comments.
243         
244         It returns ``True`` for spam and ``False`` for ham.
245         
246         If you set ``DEBUG=True`` then it will return the text of the response,
247         instead of the ``True`` or ``False`` object.
248         
249         It raises ``APIKeyError`` if you have not yet set an API key.
250         
251         If the connection to Akismet fails then the ``HTTPError`` or
252         ``URLError`` will be propogated.
253         
254         As a minimum it requires the body of the comment. This is the
255         ``comment`` argument.
256         
257         Akismet requires some other arguments, and allows some optional ones.
258         The more information you give it, the more likely it is to be able to
259         make an accurate diagnosise.
260         
261         You supply these values using a mapping object (dictionary) as the
262         ``data`` argument.
263         
264         If ``build_data`` is ``True`` (the default), then *akismet.py* will
265         attempt to fill in as much information as possible, using default
266         values where necessary. This is particularly useful for programs
267         running in a {acro;CGI} environment. A lot of useful information
268         can be supplied from evironment variables (``os.environ``). See below.
269         
270         You *only* need supply values for which you don't want defaults filled
271         in for. All values must be strings.
272         
273         There are a few required values. If they are not supplied, and
274         defaults can't be worked out, then an ``AkismetError`` is raised.
275         
276         If you set ``build_data=False`` and a required value is missing an
277         ``AkismetError`` will also be raised.
278         
279         The normal values (and defaults) are as follows : ::
280         
281             'user_ip':          os.environ['REMOTE_ADDR']       (*)
282             'user_agent':       os.environ['HTTP_USER_AGENT']   (*)
283             'referrer':         os.environ.get('HTTP_REFERER', 'unknown') [#]_
284             'permalink':        ''
285             'comment_type':     'comment' [#]_
286             'comment_author':   ''
287             'comment_author_email': ''
288             'comment_author_url': ''
289             'SERVER_ADDR':      os.environ.get('SERVER_ADDR', '')
290             'SERVER_ADMIN':     os.environ.get('SERVER_ADMIN', '')
291             'SERVER_NAME':      os.environ.get('SERVER_NAME', '')
292             'SERVER_PORT':      os.environ.get('SERVER_PORT', '')
293             'SERVER_SIGNATURE': os.environ.get('SERVER_SIGNATURE', '')
294             'SERVER_SOFTWARE':  os.environ.get('SERVER_SOFTWARE', '')
295             'HTTP_ACCEPT':      os.environ.get('HTTP_ACCEPT', '')
296         
297         (*) Required values
298         
299         You may supply as many additional 'HTTP_*' type values as you wish.
300         These should correspond to the http headers sent with the request.
301         
302         .. [#] Note the spelling "referrer". This is a required value by the
303             akismet api - however, referrer information is not always
304             supplied by the browser or server. In fact the HTTP protocol
305             forbids relying on referrer information for functionality in 
306             programs.
307         .. [#] The `API docs <http://akismet.com/development/api/>`_ state that this value
308             can be " *blank, comment, trackback, pingback, or a made up value*
309             *like 'registration'* ".
310         """
311         if self.key is None:
312             raise APIKeyError("Your have not set an API key.")
313         if data is None:
314             data = {}
315         if build_data:
316             self._build_data(comment, data)
317         if 'blog' not in data:
318             data['blog'] = self.blog_url
319         url = '%scomment-check' % self._getURL()
320         # we *don't* trap the error here
321         # so if akismet is down it will raise an HTTPError or URLError
322         headers = {'User-Agent' : self.user_agent}
323         resp = self._safeRequest(url, urlencode(data), headers)
324         if DEBUG:
325             return resp
326         resp = resp.lower()
327         if resp == 'true':
328             return True
329         elif resp == 'false':
330             return False
331         else:
332             # NOTE: Happens when you get a 'howdy wilbur' response !
333             raise AkismetError('missing required argument.')
334
335
336     def submit_spam(self, comment, data=None, build_data=True):
337         """
338         This function is used to tell akismet that a comment it marked as ham,
339         is really spam.
340         
341         It takes all the same arguments as ``comment_check``, except for
342         *DEBUG*.
343         """
344         if self.key is None:
345             raise APIKeyError("Your have not set an API key.")
346         if data is None:
347             data = {}
348         if build_data:
349             self._build_data(comment, data)
350         url = '%ssubmit-spam' % self._getURL()
351         # we *don't* trap the error here
352         # so if akismet is down it will raise an HTTPError or URLError
353         headers = {'User-Agent' : self.user_agent}
354         self._safeRequest(url, urlencode(data), headers)
355
356
357     def submit_ham(self, comment, data=None, build_data=True):
358         """
359         This function is used to tell akismet that a comment it marked as spam,
360         is really ham.
361         
362         It takes all the same arguments as ``comment_check``, except for
363         *DEBUG*.
364         """
365         if self.key is None:
366             raise APIKeyError("Your have not set an API key.")
367         if data is None:
368             data = {}
369         if build_data:
370             self._build_data(comment, data)
371         url = '%ssubmit-ham' % self._getURL()
372         # we *don't* trap the error here
373         # so if akismet is down it will raise an HTTPError or URLError
374         headers = {'User-Agent' : self.user_agent}
375         self._safeRequest(url, urlencode(data), headers)