Akismet API

I was checking my email and I saw that I had 16 new messages. This was kind of odd because it wasn’t that long since I had last checked my email. Shortly thereafter, I had this great idea to see if I could somehow integrate the Akismet antispam technology into the Dunstan-style Error Page. Little did I know how easy it woud actually be. That’s right, how easy.

Paul Duncan had already created a class (or at least most of a class) that handled all the communication. I also have to give props to the Akismet team for not only creating a well documented API, but for also making the API so easy to use. There are only four values that are ever passed back from the Akismet server to the user: true, false, valid, invalid.

But I thought I’d share the inner works of the modified Akismet class I use for the Error Plugin. I’ve broken the class down into, what I think, are logicical units:

function Akismet($api_key, $blog) {
	$this->api_key = $api_key;
	$this->blog = $blog;
	$this->required_keys = array('user_ip', 'user_agent');
}

This is the keystone for the entire class and is the first thing that is declared when starting to use the class. Akismet almost always requires four pieces of information: your API key, your blog URL, the IP address of the sender, and the User-Agent of the sender. There is one exception to this which I will discus later. In the mean time, this function takes the API Key and your blog URL and as a function and sets the array required_keys to user_ip and user_agent. If this doesn’t make sense, think about how you might perform multiple checks at a single time. You can declare this function once and then make multiple calls to it.

function check_comment($post_args) {
	$this->verify_post_args($post_args);
	return ($this->call('comment-check', $post_args, "{$this->api_key}.rest.akismet.com/1.1") != 'false');
}

function submit_spam($post_args) {
	$this->verify_post_args($post_args);
	return ($this->call('submit-spam', $post_args, "{$this->api_key}.rest.akismet.com/1.1") != 'false');
}

function submit_ham($post_args) {
	$this->verify_post_args($post_args);
	return ($this->call('submit-ham', $post_args, "{$this->api_key}.rest.akismet.com/1.1") != 'false');
}

These three functions all work the exact same way. A comment is submitted to the function and response is received. check_comment checks to see if a comment is spam; if it is, the function returns true; if the comment is not spam, the function returns false. submit_ham (i.e. good comment) and submit_spam (i.e. bad comment) lets you submit a comment to the database that was incorrectly marked as spam (thus submit_ham would be used) or not detected as spam (thus submit_spam would be used). If the submission is successful, true is returned.

The comment is separted into arguments:

  • blog (required)
    The front page or home URL of the instance making the request. For a blog or wiki this would be the front page.
  • user_ip (required)
    IP address of the comment submitter.
  • user_agent (required)
    User agent information.
  • referrer (note spelling)
    The content of the HTTP_REFERER header should be sent here.
  • permalink
    The permanent location of the entry the comment was submitted to.
  • comment_type
    May be blank, comment, trackback, pingback, or a made up value like “registration”.
  • comment_author
    Submitted name with the comment
  • comment_author_email
    Submitted email address
  • comment_author_url
    Commenter URL.
  • comment_content
    The content that was submitted.

(Source: http://akismet.com/development/api/)

This class takes these functions as an array, checks to make sure the required arguments are present, and them passes them to the function.
To make sure the required arguments are present, $this->verify_post_args($post_args); is called. This is talked about a couple functions down, but all that you need to know now is that it will do nothing if the required arguments are present or kill the script if the they are not.

Assuming the the script is not killed, the call function is executed and the game is afoot. But, again, we’re getting a head of ourselves. All that you really need to know for right now is that call takes three arguments and returns a boolean value.

  
function verify_key() {
	$sendKey = array('key' => $this->api_key);
	return ($this->call('verify-key', $sendKey, "rest.akismet.com/1.1") != 'invalid');  
}

verify_key is the only call that can be made without a valid API Key. Thus, no checking is done. This obviously makes sense because you are checking to see if a given API key is valid. If you already knew you had a valid API key, you wouldn’t need this function. In this class, the API Key is set when the class is initially declared. Thus, this function does not require any arguments.

function verify_post_args($post_args) {
	# iterate over required keys and verify each one
	foreach ($this->required_keys as $key)
	if (!array_key_exists($key, $post_args))
		die("missing required akismet key '$key'");
}

verify_post checks to make sure that the required arguments are present. For our needs, only two of four required variables need to be checked because the first two are required to declare the class. So we check that the comment has an IP address and User-Agent string. If it does, then we do nothing and let things go on as normal. If the comment doesn’t have either or both of the required arguments, the script is stopped via the die() function.

function call($meth, $post_args, $host) {
	# build post URL
	$url = "http://$host/$meth";

	# add blog to post args
	$post_args['blog'] = $this->blog;

	# init HTTP handle
	$http = curl_init($url);

	# init HTTP handle
	curl_setopt($http, CURLOPT_POST, 1);
	curl_setopt($http, CURLOPT_POSTFIELDS, $post_args);
	curl_setopt($http, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($http, CURLOPT_USERAGENT, "User-Agent: WordPress/".get_bloginfo('version')." | afdn_errorPage/$pluginVersion");
    
	# do HTTP 
	$ret = curl_exec($http);

	# check error response
	if ($err_str = curl_error($http))
		die("CURL Error: $err_str");

	# close HTTP connection
	curl_close($http);
	
	# return result
	return $ret;
}

And finally everything is put together and the call is made! If you look at one of the four method we can use (submit_ham, submit_spam, comment_check, and verify_key), they all return a value dictated by $this->call. call takes three variables: method ($meth), comment arguments ($post_args), and host ($host).

We’ve already gone over the four methods we can call. This is string is will now be used as part of the URL during the actual call.

The arguments were described above. We’ve already checked them to make sure the required ones are present, so we’re now just passing them as a POST.

Host just is also just part of the URL and will be the same 3 out of 4 times, the fourth time being when (you guessed it) we make an API call. So why is the URL different when we want to check the API key? Because all requests use your API key as part of the URL (I have no idea why, but that’s the way it works), when your checking an API Key…well, you get the idea.

So the rest should be pretty simple and straight foward. Build the URL based on the host and method. Add the blog url to the list of arguments to be passed. Then use cURL to the process the request. This isn’t a tour of cURL, so I skip that part (and perhaps write about it later because cURL is awesome). Needless to say, the request is build, sent, and an answer is received and returned to the function.

Update
Couple of things. First, I somehow made it on the Akismet blog, sweet. I also realized I never showed a complete version of the class and I never showed how to actually use the class.

So here’s the complete class:

class Akismet {
  var $version;

  function Akismet($api_key, $blog) {
    $this->api_key = $api_key;
    $this->blog = $blog;

    $this->required_keys = array('user_ip', 'user_agent');
  }

  function check_comment($post_args) {
    $this->verify_post_args($post_args);
    return ($this->call('comment-check', $post_args, "{$this->api_key}.rest.akismet.com/1.1") != 'false');
  }

  function submit_spam($post_args) {
    $this->verify_post_args($post_args);
    return ($this->call('submit-spam', $post_args, "{$this->api_key}.rest.akismet.com/1.1") != 'false');
  }

  function submit_ham($post_args) {
    $this->verify_post_args($post_args);
    return ($this->call('submit-ham', $post_args, "{$this->api_key}.rest.akismet.com/1.1") != 'false');
  }
  
  function verify_key() {
  	$sendKey = array('key' => $this->api_key);
	return ($this->call('verify-key', $sendKey, "rest.akismet.com/1.1") != 'invalid');  
  }

  function verify_post_args($post_args) {
    # iterate over required keys and verify each one
    foreach ($this->required_keys as $key)
      if (!array_key_exists($key, $post_args))
        die("missing required akismet key '$key'");
  }

  function call($meth, $post_args, $host) {
    # build post URL
    $url = "http://$host/$meth";
	//{$this->api_key}.rest.akismet.com/1.1

    # add blog to post args
    $post_args['blog'] = $this->blog;

    # init HTTP handle
    $http = curl_init($url);

    # init HTTP handle
    curl_setopt($http, CURLOPT_POST, 1);
    curl_setopt($http, CURLOPT_POSTFIELDS, $post_args);
    curl_setopt($http, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($http, CURLOPT_USERAGENT, "User-Agent: WordPress/".get_bloginfo('version')." | afdn_errorPage/$pluginVersion");
    
    # do HTTP 
    $ret = curl_exec($http);

    # check error response
    if ($err_str = curl_error($http))
      die("CURL Error: $err_str");

    # close HTTP connection
    curl_close($http);
	
    # return result
    return $ret;
  }
}

Using it is also pretty easy. Here are two functions that I use so far in the error page:

function is_comment_spam($name, $email, $comment) {
 //Load options
 $afdn_error_page_getOptions = get_option("afdn_error_page");

 // Check to see if the key is present
 if($afdn_error_page_getOptions['akismetKey'] == NULL)
  return "You have not entered a valid key!";

 # populate comment information
 $comment_data = array(
  'user_ip'               => $_SERVER['REMOTE_ADDR'],
  'user_agent'            => $_SERVER['HTTP_USER_AGENT'],
  'referrer'              => $_REQUEST['REFERER'],
  'comment_type'          => 'error_report',
  'comment_author'        => $name,
  'comment_author_email'  => $email,
  'comment_content'       => $comment,
  );

 # create akismet handle
 $ak = new Akismet($afdn_error_page_getOptions['akismetKey'], get_bloginfo('url'));
 
 # return akismet result (true for spam, false for ham)
 if($ak->check_comment($comment_data))
  return "Yes, this is spam";
 else
  return "No, this is not spam";
}

Hopefully the only thing that might look foreign to you is $afdn_error_page_getOptions = get_option("afdn_error_page");. This is a WordPress specific function that loads data from the wp_options table. The next little bit checks to see if the akismetKey has been set in the options table. If it hasn’t, then we don’t want to make a request to the API server, at least for this function. Thus, the functions returns a message saying just as much.

Assuming the key is present, we can know that it is valid becuase the key data isn’t written to the options table unless it is valid.

An array ($comment_data) is created to store all of the arguments we want to pass. Then the real fun begins. A handle is created, this gets the ball rolling by passing the API key and the blog URL. We then use the handle to check a comment for spam. I really don’t think it gets any easier then this, folks.

function is_key_valid($keyID){
	
 //Create Akismet handle
 $ak = new Akismet($keyID, get_bloginfo('url'));

 //Check key
 if($ak->verify_key())
  return true;
 else
  return false;
}

Almost the exact same as before. We don’t have a key yet, so don’t check if a key is present. There are no aruments to pass, so no array is needed. And instead of passing a known good key during the creation of the handle, the API key that we want to check for validity is passed instead.

[tags]akismet, api, documentation, php 4, php, class[/tags]

9 Replies to “Akismet API”

  1. Can’t they just send a million reports? Click the back putton and submit another report. A spammer could have a script do it automaticly. It just happened to me the other day and I ended up with over 8000 emails. 🙁 Luckly he used the same ip address from all so it was easy to remove with a single simple sql query.

Leave a Reply

Your email address will not be published. Required fields are marked *