4 # Copyright Michael Foord 2005-2009
6 # Python interface to the akismet API
7 # E-mail fuzzyman@voidspace.org.uk
9 # http://www.voidspace.org.uk/python/modules.shtml
12 # Released subject to the BSD License
13 # See http://www.voidspace.org.uk/python/license.shtml
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
21 You will need a Wordpress API key, from `wordpress.com <http://wordpress.com>`_.
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
29 Python Interface by Fuzzyman | akismet.py/0.2.0
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.
36 from akismet import Akismet
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()
44 print "No 'apikey.txt' file."
45 elif not api.verify_key():
46 print "The API key is invalid."
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.'
54 print 'This comment is ham.'
59 from urllib import urlencode
60 from django.conf import settings
61 from forum import settings
64 if hasattr(socket, 'setdefaulttimeout'):
65 # Set the default timeout on sockets to 5 seconds
66 socket.setdefaulttimeout(5)
77 __author__ = 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>'
79 __docformat__ = "restructuredtext en"
81 user_agent = "%s | akismet.py/%s"
82 DEFAULTAGENT = 'Python Interface by Fuzzyman/%s'
84 isfile = os.path.isfile
88 from google.appengine.api import urlfetch
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:
97 raise Exception('Could not fetch Akismet URL: %s Response code: %s' %
98 (url, req.status_code))
100 def _fetch_url(url, data, headers):
101 req = urllib2.Request(url, data, headers)
102 h = urllib2.urlopen(req)
107 class AkismetError(Exception):
108 """Base class for all akismet exceptions."""
110 class APIKeyError(AkismetError):
111 """Invalid API key."""
113 class Akismet(object):
114 """A class for working with the akismet API"""
116 baseurl = 'rest.akismet.com/1.1/'
118 def __init__(self, key=None, blog_url=None, agent=None):
119 """Automatically calls ``setAPIKey``."""
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)
130 Fetch the url to make requests to.
132 This comprises of api key plus the baseurl.
134 return 'http://%s.%s' % (self.key, self.baseurl)
137 def _safeRequest(self, url, data, headers):
139 resp = _fetch_url(url, data, headers)
141 raise AkismetError(str(e))
145 def setAPIKey(self, key=None, blog_url=None):
147 Set the wordpress API key for all transactions.
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
153 This method is *usually* called automatically when you create a new
154 ``Akismet`` instance.
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('#')]
160 self.key = the_file[0]
161 self.blog_url = the_file[1]
163 raise APIKeyError("Your 'apikey.txt' is invalid.")
165 self.key = settings.WORDPRESS_API_KEY
166 self.blog_url = blog_url
169 def verify_key(self):
171 This equates to the ``verify-key`` call against the akismet API.
173 It returns ``True`` if the key is valid.
175 The docs state that you *ought* to call this at the start of the
178 It raises ``APIKeyError`` if you have not yet set an API key.
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>`_)
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':
198 def _build_data(self, comment, data):
200 This function builds the data structure required by ``comment_check``,
201 ``submit_spam``, and ``submit_ham``.
203 It modifies the ``data`` dictionary you give it in place. (and so
204 doesn't return anything)
206 It raises an ``AkismetError`` if the user IP or user-agent can't be
209 data['comment_content'] = comment
210 if not 'user_ip' in data:
212 val = os.environ['REMOTE_ADDR']
214 raise AkismetError("No 'user_ip' supplied")
215 data['user_ip'] = val
216 if not 'user_agent' in data:
218 val = os.environ['HTTP_USER_AGENT']
220 raise AkismetError("No 'user_agent' supplied")
221 data['user_agent'] = val
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',
235 data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
237 data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
238 data.setdefault('blog', self.blog_url)
241 def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
243 This is the function that checks comments.
245 It returns ``True`` for spam and ``False`` for ham.
247 If you set ``DEBUG=True`` then it will return the text of the response,
248 instead of the ``True`` or ``False`` object.
250 It raises ``APIKeyError`` if you have not yet set an API key.
252 If the connection to Akismet fails then the ``HTTPError`` or
253 ``URLError`` will be propogated.
255 As a minimum it requires the body of the comment. This is the
256 ``comment`` argument.
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.
262 You supply these values using a mapping object (dictionary) as the
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.
271 You *only* need supply values for which you don't want defaults filled
272 in for. All values must be strings.
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.
277 If you set ``build_data=False`` and a required value is missing an
278 ``AkismetError`` will also be raised.
280 The normal values (and defaults) are as follows : ::
282 'user_ip': os.environ['REMOTE_ADDR'] (*)
283 'user_agent': os.environ['HTTP_USER_AGENT'] (*)
284 'referrer': os.environ.get('HTTP_REFERER', 'unknown') [#]_
286 'comment_type': 'comment' [#]_
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', '')
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.
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
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'* ".
313 raise APIKeyError("Your have not set an API key.")
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)
330 elif resp == 'false':
333 # NOTE: Happens when you get a 'howdy wilbur' response !
334 raise AkismetError('missing required argument.')
337 def submit_spam(self, comment, data=None, build_data=True):
339 This function is used to tell akismet that a comment it marked as ham,
342 It takes all the same arguments as ``comment_check``, except for
346 raise APIKeyError("Your have not set an API key.")
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)
358 def submit_ham(self, comment, data=None, build_data=True):
360 This function is used to tell akismet that a comment it marked as spam,
363 It takes all the same arguments as ``comment_check``, except for
367 raise APIKeyError("Your have not set an API key.")
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)