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.key = settings.WORDPRESS_API_KEY
123 self.blog_url = settings.WORDPRESS_BLOG_URL
128 Fetch the url to make requests to.
130 This comprises of api key plus the baseurl.
132 return 'http://%s.%s' % (self.key, self.baseurl)
135 def _safeRequest(self, url, data, headers):
137 resp = _fetch_url(url, data, headers)
139 raise AkismetError(str(e))
143 def setAPIKey(self, key=None, blog_url=None):
145 Set the wordpress API key for all transactions.
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
151 This method is *usually* called automatically when you create a new
152 ``Akismet`` instance.
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('#')]
158 self.key = the_file[0]
159 self.blog_url = the_file[1]
161 raise APIKeyError("Your 'apikey.txt' is invalid.")
163 self.key = settings.WORDPRESS_API_KEY
164 self.blog_url = blog_url
167 def verify_key(self):
169 This equates to the ``verify-key`` call against the akismet API.
171 It returns ``True`` if the key is valid.
173 The docs state that you *ought* to call this at the start of the
176 It raises ``APIKeyError`` if you have not yet set an API key.
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>`_)
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':
196 def _build_data(self, comment, data):
198 This function builds the data structure required by ``comment_check``,
199 ``submit_spam``, and ``submit_ham``.
201 It modifies the ``data`` dictionary you give it in place. (and so
202 doesn't return anything)
204 It raises an ``AkismetError`` if the user IP or user-agent can't be
207 data['comment_content'] = comment
208 if not 'user_ip' in data:
210 val = os.environ['REMOTE_ADDR']
212 raise AkismetError("No 'user_ip' supplied")
213 data['user_ip'] = val
214 if not 'user_agent' in data:
216 val = os.environ['HTTP_USER_AGENT']
218 raise AkismetError("No 'user_agent' supplied")
219 data['user_agent'] = val
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',
233 data.setdefault('SERVER_SOFTWARE', os.environ.get('SERVER_SOFTWARE',
235 data.setdefault('HTTP_ACCEPT', os.environ.get('HTTP_ACCEPT', ''))
236 data.setdefault('blog', self.blog_url)
239 def comment_check(self, comment, data=None, build_data=True, DEBUG=False):
241 This is the function that checks comments.
243 It returns ``True`` for spam and ``False`` for ham.
245 If you set ``DEBUG=True`` then it will return the text of the response,
246 instead of the ``True`` or ``False`` object.
248 It raises ``APIKeyError`` if you have not yet set an API key.
250 If the connection to Akismet fails then the ``HTTPError`` or
251 ``URLError`` will be propogated.
253 As a minimum it requires the body of the comment. This is the
254 ``comment`` argument.
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.
260 You supply these values using a mapping object (dictionary) as the
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.
269 You *only* need supply values for which you don't want defaults filled
270 in for. All values must be strings.
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.
275 If you set ``build_data=False`` and a required value is missing an
276 ``AkismetError`` will also be raised.
278 The normal values (and defaults) are as follows : ::
280 'user_ip': os.environ['REMOTE_ADDR'] (*)
281 'user_agent': os.environ['HTTP_USER_AGENT'] (*)
282 'referrer': os.environ.get('HTTP_REFERER', 'unknown') [#]_
284 'comment_type': 'comment' [#]_
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', '')
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.
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
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'* ".
311 raise APIKeyError("Your have not set an API key.")
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)
328 elif resp == 'false':
331 # NOTE: Happens when you get a 'howdy wilbur' response !
332 raise AkismetError('missing required argument.')
335 def submit_spam(self, comment, data=None, build_data=True):
337 This function is used to tell akismet that a comment it marked as ham,
340 It takes all the same arguments as ``comment_check``, except for
344 raise APIKeyError("Your have not set an API key.")
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)
356 def submit_ham(self, comment, data=None, build_data=True):
358 This function is used to tell akismet that a comment it marked as spam,
361 It takes all the same arguments as ``comment_check``, except for
365 raise APIKeyError("Your have not set an API key.")
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)