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 forum import settings
63 if hasattr(socket, 'setdefaulttimeout'):
64 # Set the default timeout on sockets to 5 seconds
65 socket.setdefaulttimeout(5)
76 __author__ = 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>'
78 __docformat__ = "restructuredtext en"
80 user_agent = "%s | akismet.py/%s"
81 DEFAULTAGENT = 'Python Interface by Fuzzyman/%s'
83 isfile = os.path.isfile
87 from google.appengine.api import urlfetch
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:
96 raise Exception('Could not fetch Akismet URL: %s Response code: %s' %
97 (url, req.status_code))
99 def _fetch_url(url, data, headers):
100 req = urllib2.Request(url, data, headers)
101 h = urllib2.urlopen(req)
106 class AkismetError(Exception):
107 """Base class for all akismet exceptions."""
109 class APIKeyError(AkismetError):
110 """Invalid API key."""
112 class Akismet(object):
113 """A class for working with the akismet API"""
115 baseurl = 'rest.akismet.com/1.1/'
117 def __init__(self, key=None, blog_url=None, agent=None):
118 """Automatically calls ``setAPIKey``."""
120 agent = DEFAULTAGENT % __version__
121 self.user_agent = user_agent % (agent, __version__)
122 self.setAPIKey(key, blog_url)
127 Fetch the url to make requests to.
129 This comprises of api key plus the baseurl.
131 return 'http://%s.%s' % (self.key, self.baseurl)
134 def _safeRequest(self, url, data, headers):
136 resp = _fetch_url(url, data, headers)
138 raise AkismetError(str(e))
142 def setAPIKey(self, key=None, blog_url=None):
144 Set the wordpress API key for all transactions.
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
150 This method is *usually* called automatically when you create a new
151 ``Akismet`` instance.
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('#')]
157 self.key = the_file[0]
158 self.blog_url = the_file[1]
160 raise APIKeyError("Your 'apikey.txt' is invalid.")
163 self.blog_url = blog_url
166 def verify_key(self):
168 This equates to the ``verify-key`` call against the akismet API.
170 It returns ``True`` if the key is valid.
172 The docs state that you *ought* to call this at the start of the
175 It raises ``APIKeyError`` if you have not yet set an API key.
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>`_)
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':
195 def _build_data(self, comment, data):
197 This function builds the data structure required by ``comment_check``,
198 ``submit_spam``, and ``submit_ham``.
200 It modifies the ``data`` dictionary you give it in place. (and so
201 doesn't return anything)
203 It raises an ``AkismetError`` if the user IP or user-agent can't be
206 data['comment_content'] = comment
207 if not 'user_ip' in data:
209 val = os.environ['REMOTE_ADDR']
211 raise AkismetError("No 'user_ip' supplied")
212 data['user_ip'] = val
213 if not 'user_agent' in data:
215 val = os.environ['HTTP_USER_AGENT']
217 raise AkismetError("No 'user_agent' supplied")
218 data['user_agent'] = val
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',
232 data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
234 data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
235 data.setdefault('blog', self.blog_url)
238 def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
240 This is the function that checks comments.
242 It returns ``True`` for spam and ``False`` for ham.
244 If you set ``DEBUG=True`` then it will return the text of the response,
245 instead of the ``True`` or ``False`` object.
247 It raises ``APIKeyError`` if you have not yet set an API key.
249 If the connection to Akismet fails then the ``HTTPError`` or
250 ``URLError`` will be propogated.
252 As a minimum it requires the body of the comment. This is the
253 ``comment`` argument.
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.
259 You supply these values using a mapping object (dictionary) as the
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.
268 You *only* need supply values for which you don't want defaults filled
269 in for. All values must be strings.
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.
274 If you set ``build_data=False`` and a required value is missing an
275 ``AkismetError`` will also be raised.
277 The normal values (and defaults) are as follows : ::
279 'user_ip': os.environ['REMOTE_ADDR'] (*)
280 'user_agent': os.environ['HTTP_USER_AGENT'] (*)
281 'referrer': os.environ.get('HTTP_REFERER', 'unknown') [#]_
283 'comment_type': 'comment' [#]_
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', '')
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.
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
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'* ".
310 raise APIKeyError("Your have not set an API key.")
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)
327 elif resp == 'false':
330 # NOTE: Happens when you get a 'howdy wilbur' response !
331 raise AkismetError('missing required argument.')
334 def submit_spam(self, comment, data=None, build_data=True):
336 This function is used to tell akismet that a comment it marked as ham,
339 It takes all the same arguments as ``comment_check``, except for
343 raise APIKeyError("Your have not set an API key.")
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)
355 def submit_ham(self, comment, data=None, build_data=True):
357 This function is used to tell akismet that a comment it marked as spam,
360 It takes all the same arguments as ``comment_check``, except for
364 raise APIKeyError("Your have not set an API key.")
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)