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