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
129 Fetch the url to make requests to.
131 This comprises of api key plus the baseurl.
133 return 'http://%s.%s' % (self.key, self.baseurl)
136 def _safeRequest(self, url, data, headers):
138 resp = _fetch_url(url, data, headers)
140 raise AkismetError(str(e))
144 def setAPIKey(self, key=None, blog_url=None):
146 Set the wordpress API key for all transactions.
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
152 This method is *usually* called automatically when you create a new
153 ``Akismet`` instance.
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('#')]
159 self.key = the_file[0]
160 self.blog_url = the_file[1]
162 raise APIKeyError("Your 'apikey.txt' is invalid.")
164 self.key = settings.WORDPRESS_API_KEY
165 self.blog_url = blog_url
168 def verify_key(self):
170 This equates to the ``verify-key`` call against the akismet API.
172 It returns ``True`` if the key is valid.
174 The docs state that you *ought* to call this at the start of the
177 It raises ``APIKeyError`` if you have not yet set an API key.
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>`_)
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':
197 def _build_data(self, comment, data):
199 This function builds the data structure required by ``comment_check``,
200 ``submit_spam``, and ``submit_ham``.
202 It modifies the ``data`` dictionary you give it in place. (and so
203 doesn't return anything)
205 It raises an ``AkismetError`` if the user IP or user-agent can't be
208 data['comment_content'] = comment
209 if not 'user_ip' in data:
211 val = os.environ['REMOTE_ADDR']
213 raise AkismetError("No 'user_ip' supplied")
214 data['user_ip'] = val
215 if not 'user_agent' in data:
217 val = os.environ['HTTP_USER_AGENT']
219 raise AkismetError("No 'user_agent' supplied")
220 data['user_agent'] = val
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',
234 data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
236 data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
237 data.setdefault('blog', self.blog_url)
240 def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
242 This is the function that checks comments.
244 It returns ``True`` for spam and ``False`` for ham.
246 If you set ``DEBUG=True`` then it will return the text of the response,
247 instead of the ``True`` or ``False`` object.
249 It raises ``APIKeyError`` if you have not yet set an API key.
251 If the connection to Akismet fails then the ``HTTPError`` or
252 ``URLError`` will be propogated.
254 As a minimum it requires the body of the comment. This is the
255 ``comment`` argument.
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.
261 You supply these values using a mapping object (dictionary) as the
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.
270 You *only* need supply values for which you don't want defaults filled
271 in for. All values must be strings.
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.
276 If you set ``build_data=False`` and a required value is missing an
277 ``AkismetError`` will also be raised.
279 The normal values (and defaults) are as follows : ::
281 'user_ip': os.environ['REMOTE_ADDR'] (*)
282 'user_agent': os.environ['HTTP_USER_AGENT'] (*)
283 'referrer': os.environ.get('HTTP_REFERER', 'unknown') [#]_
285 'comment_type': 'comment' [#]_
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', '')
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.
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
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'* ".
312 raise APIKeyError("Your have not set an API key.")
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)
329 elif resp == 'false':
332 # NOTE: Happens when you get a 'howdy wilbur' response !
333 raise AkismetError('missing required argument.')
336 def submit_spam(self, comment, data=None, build_data=True):
338 This function is used to tell akismet that a comment it marked as ham,
341 It takes all the same arguments as ``comment_check``, except for
345 raise APIKeyError("Your have not set an API key.")
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)
357 def submit_ham(self, comment, data=None, build_data=True):
359 This function is used to tell akismet that a comment it marked as spam,
362 It takes all the same arguments as ``comment_check``, except for
366 raise APIKeyError("Your have not set an API key.")
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)