#65
Aug 06, 2007

Stopping Spam with Akismet

The Railscasts site has been getting a lot of comment spam in the past, but no longer. In this episode I will show you how I solved this problem by using the Akismet web service.
Download (44.4 MB, 11:17)
alternative download for iPod & Apple TV (20.7 MB, 11:17)

Resources

# comments_controller.rb
def create
  @comment = Comment.new(params[:comment])
  @comment.request = request
  if @comment.save
    if @comment.approved?
      flash[:notice] = "Thanks for the comment."
    else
      flash[:error] = "Unfortunately this comment is considered spam by Akismet. " + 
                      "It will show up once it has been approved by the administrator."
    end
    redirect_to episode_path(@comment.episode_id)
  else
    render :action => 'new'
  end
end

def destroy_multiple
  Comment.destroy(params[:comment_ids])
  flash[:notice] = "Successfully destroyed comments."
  redirect_to comments_path
end

def approve
  @comment = Comment.find(params[:id])
  @comment.mark_as_ham!
  redirect_to comments_path
end

def reject
  @comment = Comment.find(params[:id])
  @comment.mark_as_spam!
  redirect_to comments_path
end

# models/comment.rb
before_create :check_for_spam

def request=(request)
  self.user_ip    = request.remote_ip
  self.user_agent = request.env['HTTP_USER_AGENT']
  self.referrer   = request.env['HTTP_REFERER']
end

def check_for_spam
  self.approved = !Akismetor.spam?(akismet_attributes)
  true # return true so it doesn't stop save
end

def akismet_attributes
  {
    :key                  => 'abc123',
    :blog                 => 'http://railscasts.com',
    :user_ip              => user_ip,
    :user_agent           => user_agent,
    :comment_author       => name,
    :comment_author_email => email,
    :comment_author_url   => site_url,
    :comment_content      => content
  }
end

def mark_as_spam!
  update_attribute(:approved, false)
  Akismetor.submit_spam(akismet_attributes)
end

def mark_as_ham!
  update_attribute(:approved, true)
  Akismetor.submit_ham(akismet_attributes)
end

29 comments

chineseGuy Aug 06, 2007 at 01:45

gaert!vrey good.
tkanhs


mutle Aug 06, 2007 at 04:51

Great stuff, Ryan.
Unfortunately Akismet is pretty pricey, if you want to use it commercially. I am working for a Blog-Host, and using Akismet would cost double of what we pay for server hosting.
But what I have seen in your screencast seems like it may be worth the money. Currently we have a custom CAPTCHA, which works fine for now, except that you can't user CAPTCHAs for Trackbacks.
Anyway, thanks for this plugin, I will try it out.


pimpmaster Aug 06, 2007 at 04:52

*injects cable modem needle into arm*

Ahhhhhh...yeah that's the stuff.

*stays high on Rails for hours*


pimpmaster Aug 06, 2007 at 05:49

Just watched this again and noticed something curious.. what does an exclamation point do to an method? You have this one your mark_as_spam/ham actions.


yanyu Aug 06, 2007 at 06:18

There is also a Ruby on Rails Akismet project on Ruby Forge from Josh French. Josh French said by the README, "This plugin is based on David Czarnecki's Akismet.rb (http://www.blojsom.com/blog/david/nerdery/2005/12/02/Akismet-API-in-Ruby.html) found via Soaked and Soaped (http://sas.sparklingstudios.com/articles/2006/10/01/how-to-protect-a-rails-application-against-spam-with-akismet)"


Saxon Aug 06, 2007 at 06:55

If I'm not mistaken, the exclamation mark at the end of methods means that you are modifying a variable instead of just accessing it. For example, "foobar.sort" will return a sorted array, but leave foobar alone. "foobar.sort!" will actually replace foobar with the sorted array.

By adding an exclamation mark to the end of the method like that he's just noting that the method is changing a class variable. It's optional, but good practice.


Dr J Aug 06, 2007 at 07:10

Nice screencast. This is something I definitely need to do on a few of my sites. I've used Akismet before with PHP but never implemented it with rails yet.


Gary Aug 06, 2007 at 07:32

I recommend using defensio. It's smarter than Akismet and learns as it goes. It also has a much better API for developers.

Check it out: http://www.defensio.com/


mutle Aug 06, 2007 at 08:17

In ActiveRecord methods the exclamation mark is used to tell that it raises an exception if something goes wrong (like save!, create!).


pimpmaster Aug 06, 2007 at 08:26

Thanks for that concise explanation Saxon!

My Rails-fu went up a few points


Karl Aug 06, 2007 at 08:28

IIRC, the exclamation is just a character and does not specifically modify the behavior of any method. It has become common in the Rails framework to denote that the method does something different (as mutle noted) as opposed to a method without it. And thus many people have adopted it's use.

Same as the equals in a method name. It doesn't really change the behavior of the method, but when creating getter/setter methods it makes you code more clear. Hence for the setter you can write 'mysetter=myvalue' instead of 'mysetter myvalue'.


Ryan Bates Aug 06, 2007 at 08:32

I like to think of the exclamation mark as a warning that the method does something potentially dangerous/permanent. This way you think twice before using it and make sure that's really what you want.

Here I'm using it because the method is contacting Akismet in addition to changing the model's state. This method shouldn't be called lightly.


Wincent Colaiuta Aug 07, 2007 at 07:28

Ryan, shouldn't you have some kind of "is_admin?" before filter protecting your destroy_multiple/approve/reject actions?


Ryan Bates Aug 07, 2007 at 09:29

@Wincent, that's already done. If you look in the controller on the screencast you can see there's an "authorize" before filter.


Wes Aug 07, 2007 at 11:34

Did you add an index for comments.approved? Just curious.

Just to note, methods ending in ? typically return true or false. Again, it's convention, not a requirement. It just makes it easier to read.


Ryan Bates Aug 07, 2007 at 13:59

@Wes, are you asking where the "approved?" method comes from? This is generated automatically by Rails. Since there's a boolean "approved" attribute in the database, it gives us the "approved?" method which will return true/false.

There's also a class method called "approved" which just does a find for all comments which are approved. I use this for listing the comments for each episode. It's completely separate from the "approved?" instance method.


John Aug 09, 2007 at 15:52

Hi Ryan,

I've also been having the same issues with spam on my blog, I've followed your code down to the last line and implemented it in my app. I've also tested your code by pasting in some of the spam that has hit my comment (the spam comment before this one is mine) but as you can see the spam isn't getting blocked. Please advise on way forward.


john Aug 10, 2007 at 00:52

Ryan, I decided to test this further by pasting the same spam comments on other blogs that use Akismet such as www.techcrunch.com (wordpress blog), none of the spam comments were saved. Probably I don't understand the inner workings of Akismet but I was of the impression that it also checks for certain patterns like a high number of links in the content in order to flag it as spam, may be you could correct me on this.


Ryan Bates Aug 10, 2007 at 08:53

@john, Akismet learns as it goes, so it may have flagged your IP address as a possible spammer. That's my guess, but there could be a number of other factors coming into play.

Either way, I don't recommend testing this out because the last thing you want is to be considered a spammer by Akismet and then everything you post on other blogs will be flagged.


Mike Aug 10, 2007 at 20:39

Not on-topic, but it would be awesome if you could do a video on how to create in place editing. The Rails built-in methods are apparently being deprecated in 2.0. There evidently will (is?) a plugin alternative, but I can't find any information.

No one seems to know what the best practice is right now in terms of in-place editing. Your my hero, Ryan! :)


Douglas F Shearer Aug 20, 2007 at 14:31

Great screencast Ryan!

Also good to see that someone has finally made a plugin available that wraps up the Akismet functionality. For a year or so now I've been using a heavily modified version of David Czarnecki's akismet.rb to fulfil this functionality.

@mutle - In the past I've attempted to create my own spam filtering, using an interface that accepts regexps, and monitoring the performance of these to make adjustments. In the end it was a lot of work both in implementation and time to administrate. I'm tempted to have a go again though.


ofer Aug 26, 2007 at 23:24

Slightly off topic. Curious about the use of custom RESTful actions:

From reading the on-line literature - Purist seem to say you can always adhere to the default actions by adding new controllers.

In your case why aren't you following the pure RESTful pattern and if you were would you build an approval controller where "create" is approving and "destroy" is marking as spam ?


Ryan Bates Aug 26, 2007 at 23:58

@ofer, good point. I could create a "comment_approval" RESTful controller and avoid the custom REST actions. However, I'm no REST purist. I try to weigh the practicality of the different approaches. I consider the approve/reject actions to be largely about the comment resource (just different forms of update) so I decided to keep them in that controller.

I also like to follow the "simplest thing that could possible work" principle. Adding custom actions feels simpler. However, it's also a bit of a smell and could require some refactoring. The stench isn't quite strong enough IMO though.


Andri Mar Dec 16, 2007 at 15:10

Wouldn't it be nice to include the approved filter in the relationship between the Post and the Comment. Like this:
has_many( :comments, :class_name => "Comment", :conditions => {:approved => true}, :dependent => :destroy )
Then the simplify the finding in the controller as the condition would be built into the association so a simple find(:all) would suffice. Just my $0.02. Anyway a great screencast and keep up the good work.


Dave Jan 05, 2008 at 19:28

This is my first comment so I want to start by saying thanks for all the screencasts. They've been a huge help.

I'm in the process of implementing this and I was wondering what would happen if Akismet was offline. I disconnected from the intertubes in my dev environment and got a socket error crash so I'll need to catch that.

Also, I can't help noticing that a couple of helpless credit card user posts got through in December. I'm wondering if it would be worth running comments against Akismet more than once. Akismet's opinion of a comment might change after a day or two as it continues to learn. I'd just be concerned about abusing the TOS.


Geoff Mar 27, 2008 at 02:28

Does anyone know if there is some kind of 'safe' spam comment you can put in?

I read somewhere that viagara_test_123 was always marked as spam, but if this comment shows up it's obviously not true..


kino May 23, 2008 at 01:35

@Geoff use Askimet filter


John May 23, 2008 at 01:37

Thanx for you work!


Matthew Bergman Jun 29, 2008 at 22:33

Geoff:

I always found that spam spam spam work pretty well.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(required)

subscribe:
sponsored by:
if you want to help:
required:
Get Quicktime Player