Right off the bat I should say that, like most Ruby applications, there are a ton of different ways this could be achieved. But there didn’t seem to be too many examples floating around, so I thought I would explain how I’m doing it on this site.
For those who don’t know what Defensio is, let me explain. It’s a simple web service that essentially filters any content you give it in order to determine if it’s “spammy” or not. Basically, you give it some content and it spits back some attributes that you can use to base decisions on.
There are 3 key attributes that you’ll find very useful: allow, profanity-match, and spaminess.
allow – returns true/false depending on whether or not Defensio thinks this piece of content should be allowed on your site
profanity-match – returns true/false based on, well, whether or not the content has profanity in it
spaminess – returns a Float value between 0 and 1 indicating how “spammy” the content is (1 would be 100% spam).
Based on these attributes, you can setup some rules to handle the flow of your comments. Here’s essentially what my rules are:
- Automatically approve the comment and allow it to go through if Defensio sets
allow to true, profanity-match to false, and a spaminess value between 0 and 0.10
- Put the comment in an approval queue if any one of the above things fail, and the
spaminess is less than 0.75
- Automatically reject the comment if the
spaminess is greater than 0.75, regardless of what the allow and profanity-match values say
With that said, here’s basically how I have things setup. Firstly…
1 |
$> sudo gem install defensio
|
It’s pretty simple to interface with the Defensio API without the gem, but why not use it, right?
In my comment model I have a before_create :check_for_spam callback that hands the comment over to Defensio. I added a DefensioResponse class that I pass the returned attributes to. That’s also where I keep the rules/logic. Here are the noteworthy pieces of my comment model:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 |
class Comment < ActiveRecord::Base
has_one :defensio_response
attr_accessor :defensio_signature
before_create :check_for_spam
after_create :associate_with_defensio_response
attr_protected :approved
protected
def check_for_spam
result = self.class.defensio.post_document(self.defensio_attributes)
status, attributes = result.first, result.last
return false unless status == 200
defensio_response = DefensioResponse.init(attributes)
self.defensio_signature = defensio_response.signature
self.approved = defensio_response.approved?
defensio_response.proceed?
end
def associate_with_defensio_response
DefensioResponse.find_by_signature(self.defensio_signature).update_attribute(:comment_id, self.id)
end
def self.defensio
@@defensio ||= Defensio.new(self.defensio_api_key)
end
def self.defensio_api_key
YAML::load(File.open(File.join(Rails.root, 'config', 'defensio.yml')))["api_key"]
end
def defensio_attributes
{
"type" => "comment",
"platform" => "rubyonrails",
"content" => self.body,
"author-email" => self.email,
"author-name" => self.name,
"author-url" => self.site
}
end
public
def approve!
self.update_attribute(:approved, true)
self.class.defensio.put_document(signature, { :allow => true })
end
def mark_as_spam!
self.update_attribute(:approved, false)
self.class.defensio.put_document(signature, { :allow => false })
end
def signature
self.defensio_response.signature
end
end
|
When you post a document to Defensio, it returns an array of two values. The first is the status code and the second is the actual output of the response (as a hash of attributes).
You can see in the check_for_spam method that I’m setting the self.approved flag based on the logic I have in my defensio_response. Like I said, if a comment looks good according to my rules, then I don’t want to be bothered with approving it and let it pass through.
For the missing pieces, here are the internals of my DefensioResponse class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 |
class DefensioResponse < ActiveRecord::Base
belongs_to :comment
validates_presence_of :allow, :spaminess, :signature
AUTOMATIC_APPROVAL_RANGE = 0.0..0.1
AUTOMATIC_DENIAL_VALUE = 0.75
private
def self.clean!(attrs)
returning({}) do |cleansed_attributes|
attrs.each do |key, value|
cleansed_attributes.merge!(key.to_s.underscore => value)
end
end
end
public
def self.init(attributes)
self.new(clean!(attributes))
end
def approved?
self.allow? &&
self.no_profanity? &&
AUTOMATIC_APPROVAL_RANGE.include?(self.spaminess)
end
def no_profanity?
!self.profanity_match?
end
def proceed?
return false if self.spaminess > AUTOMATIC_DENIAL_VALUE
self.save!
end
end
|
All the clean!(attrs) method does is change the response values, which use dashes (i.e. “profanity-match”) to use underscores (i.e. “profanity_match”), since that’s what ActiveRecord prefers.
Also, as you can see the approved? method only returns true if all 3 of my requirements are true. That’s the only way a comment will automatically be accepted.
In my comment model, the final line in the check_for_spam method is a call to the defensio_response.proceed? method. Remember, in Rails, if you return false from a callback then the entire transaction is canceled. So, in the proceed? method, if the spaminess is simply too high, I’m just returning false and canceling everything (no comment or response get saved). But if it’s good to go, I’ll let the self.save! call determine if things should be committed, because really, I don’t want a comment without a defensio response.
Improving Over Time
From the code above, the only thing the defensio gem provides is the post_document and put_document methods, which reflect a RESTful API. You may understand why you would POST a document, but why would you PUT (read: update) one? It’s simple: to report false positives/negatives.
Defensio uses a learning algorithm, and like anything that learns, you have to teach it so it becomes smarter. If you look in my comment model you’ll notice two methods: approve! and mark_as_spam!. Remember, if a comment falls between 0.1 and 0.75 then it goes into the “needs my approval” queue.
If I approve the comment, it updates the flag in the comments table, but it also tells Defensio that “this is a good comment”. Likewise, if I mark one as spam, it updates the flag and tells Defensio that “this is a bad comment”.
That’s one reason why I’m saving a Defensio response with each comment, so I have the response signature to identify the comment later on. The signature is the only common piece of information between the content on Defensio and the comment on this site.
Results
So far, Defensio has been doing great. I’ve only been using it for a few days, and here’s what it has done so far:

Already has prevented 333 spam comments at 96.07% accuracy? I’ll take that.
Like I said, I didn’t find much in terms of examples of how others approached this, so hopefully someone will find this useful. If you’re having spam trouble, I’d take a close look at Defensio. They have a simple API and an ever-increasing level of accuracy. So far I’m very pleased.