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