mikehale-akismet 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION.yml +4 -0
- data/lib/akismet.rb +64 -0
- data/spec/akismet_spec.rb +83 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/spec_http.rb +164 -0
- metadata +59 -0
data/VERSION.yml
ADDED
data/lib/akismet.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
class Akismet
|
5
|
+
USER_AGENT = "Akismet-rb/1.0 | Akismet/1.11"
|
6
|
+
|
7
|
+
def initialize(key, url)
|
8
|
+
@key = key
|
9
|
+
@url = url
|
10
|
+
end
|
11
|
+
|
12
|
+
def verify_key
|
13
|
+
response = Net::HTTP.start('rest.akismet.com', 80) do |http|
|
14
|
+
http.post('/1.1/verify-key', post_data(:key => @key, :blog => @url), {'User-Agent' => USER_AGENT})
|
15
|
+
end
|
16
|
+
|
17
|
+
case response.body
|
18
|
+
when "invalid"
|
19
|
+
raise Akismet::VerifyException, response.to_hash["x-akismet-debug-help"], caller
|
20
|
+
when "valid"
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def submit_spam(args)
|
26
|
+
call_akismet('submit-spam', args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def submit_ham(args)
|
30
|
+
call_akismet('submit-ham', args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def spam?(args)
|
34
|
+
call_akismet('comment-check', args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def ham?(args)
|
38
|
+
!spam?(args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def call_akismet(method, args)
|
42
|
+
args.update(:blog => @url)
|
43
|
+
|
44
|
+
response = Net::HTTP.start("#{@key}.rest.akismet.com", 80) do |http|
|
45
|
+
http.post("/1.1/#{method}", post_data(args), {'User-Agent' => USER_AGENT})
|
46
|
+
end
|
47
|
+
|
48
|
+
case response.body
|
49
|
+
when "true"
|
50
|
+
true
|
51
|
+
when "false"
|
52
|
+
false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def post_data(hash)
|
57
|
+
hash.inject([]) do |memo, hash|
|
58
|
+
k, v = hash
|
59
|
+
memo << "#{k}=#{URI.escape(v)}"
|
60
|
+
end.join('&')
|
61
|
+
end
|
62
|
+
|
63
|
+
class VerifyException < Exception; end
|
64
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe "Akismet" do
|
4
|
+
include SpecHttp
|
5
|
+
|
6
|
+
def params
|
7
|
+
{
|
8
|
+
:user_ip => "1.2.3.4",
|
9
|
+
:referrer => "http://othersite.com",
|
10
|
+
:permalink => "http://example.com/post",
|
11
|
+
:comment_type => "comment",
|
12
|
+
:comment_author => "joe smith",
|
13
|
+
:comment_author_email => "joe@smith.com",
|
14
|
+
:comment_author_url => "blog.smith.com"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
@akismet = Akismet.new('thekey', 'http://example.com')
|
20
|
+
app = lambda do |env|
|
21
|
+
spam = env["rack.input"].include?('viagra')
|
22
|
+
[200, {}, [spam ? "true" : "false"]]
|
23
|
+
end
|
24
|
+
map Rack::URLMap.new("http://thekey.rest.akismet.com/1.1/comment-check" => app)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should verify the key" do
|
28
|
+
map Rack::URLMap.new("http://rest.akismet.com/" => lambda { |env| [200, {}, ["valid"]]})
|
29
|
+
|
30
|
+
@akismet.verify_key.should == true
|
31
|
+
request.post?.should == true
|
32
|
+
request.env['HTTP_USER_AGENT'].should == "Akismet-rb/1.0 | Akismet/1.11"
|
33
|
+
request.body.should include('http://example.com')
|
34
|
+
response.status.should == 200
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should not verify an invalid key" do
|
38
|
+
map Rack::URLMap.new("http://rest.akismet.com/" => lambda { |env| [200, {'x-akismet-debug-help' => 'sorry!'}, ["invalid"]]})
|
39
|
+
lambda {@akismet.verify_key}.should raise_error Akismet::VerifyException
|
40
|
+
response['x-akismet-debug-help'].should == 'sorry!'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should detect spam" do
|
44
|
+
@akismet.spam?(params.update(:comment_content => "viagra-test-123")).should == true
|
45
|
+
request.env.has_key?('HTTP_USER_AGENT').should == true
|
46
|
+
request.body.should include("blog=http://example.com")
|
47
|
+
request.body.should include("user_ip=1.2.3.4")
|
48
|
+
request.body.should include("referrer=http://othersite.com")
|
49
|
+
request.body.should include("permalink=http://example.com/post")
|
50
|
+
request.body.should include("comment_type=comment")
|
51
|
+
request.body.should include("comment_author=joe%20smith")
|
52
|
+
request.body.should include("comment_author_email=joe@smith.com")
|
53
|
+
request.body.should include("comment_author_url=blog.smith.com")
|
54
|
+
request.body.should include("comment_content=viagra-test-123")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should detect ham" do
|
58
|
+
@akismet.ham?(params.update(:comment_content => "not spam")).should == true
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should submit spam" do
|
62
|
+
map Rack::URLMap.new("http://thekey.rest.akismet.com/1.1/submit-spam" => lambda { |env| [200, {}, "true"]})
|
63
|
+
@akismet.submit_spam(params.update(:comment_content => "this-is-spam"))
|
64
|
+
request.script_name.should == "/1.1/submit-spam"
|
65
|
+
|
66
|
+
request.env.has_key?('HTTP_USER_AGENT').should == true
|
67
|
+
request.body.should include("blog=http://example.com")
|
68
|
+
request.body.should include("user_ip=1.2.3.4")
|
69
|
+
request.body.should include("referrer=http://othersite.com")
|
70
|
+
request.body.should include("permalink=http://example.com/post")
|
71
|
+
request.body.should include("comment_type=comment")
|
72
|
+
request.body.should include("comment_author=joe%20smith")
|
73
|
+
request.body.should include("comment_author_email=joe@smith.com")
|
74
|
+
request.body.should include("comment_author_url=blog.smith.com")
|
75
|
+
request.body.should include("comment_content=this-is-spam")
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should submit ham" do
|
79
|
+
map Rack::URLMap.new("http://thekey.rest.akismet.com/1.1/submit-ham" => lambda { |env| [200, {}, "true"]})
|
80
|
+
@akismet.submit_ham(params.update(:comment_content => "this-is-ham"))
|
81
|
+
request.script_name.should == "/1.1/submit-ham"
|
82
|
+
end
|
83
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require 'rr'
|
3
|
+
require 'rack/urlmap'
|
4
|
+
require File.dirname(__FILE__) + '/spec_http'
|
5
|
+
require 'ruby-debug'
|
6
|
+
|
7
|
+
class MethodSpy
|
8
|
+
def initialize(delegate, &block)
|
9
|
+
@delegate = delegate
|
10
|
+
@filter = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(symbol, *args, &block)
|
14
|
+
result = @delegate.send(symbol, *args, &block)
|
15
|
+
p [symbol, args, result, block]
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
21
|
+
|
22
|
+
require 'akismet'
|
23
|
+
|
24
|
+
Spec::Runner.configure do |config|
|
25
|
+
config.mock_with :rr
|
26
|
+
end
|
data/spec/spec_http.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'rack/request'
|
3
|
+
require 'rack/response'
|
4
|
+
require 'rack/utils'
|
5
|
+
|
6
|
+
module SpecHttp
|
7
|
+
def map(map)
|
8
|
+
@socket = SocketInterceptor.new(map)
|
9
|
+
stub(TCPSocket).open(anything, numeric) { @socket }
|
10
|
+
end
|
11
|
+
|
12
|
+
def request
|
13
|
+
Rack::Request.new(@socket.env)
|
14
|
+
end
|
15
|
+
|
16
|
+
def response
|
17
|
+
@socket.response
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class SocketInterceptor < StringIO
|
22
|
+
attr_reader :env, :response
|
23
|
+
|
24
|
+
def initialize(map)
|
25
|
+
@map = map
|
26
|
+
super()
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(string)
|
30
|
+
raise "Already responded" if self.string.size > 0
|
31
|
+
@written = '' unless @written
|
32
|
+
@written << string
|
33
|
+
string.size
|
34
|
+
end
|
35
|
+
|
36
|
+
# this depends on Net::HTTP calling socket.sysread after it has finished all writes
|
37
|
+
alias :orig_sysread :sysread
|
38
|
+
def sysread(size)
|
39
|
+
set_string unless self.string.size > 0
|
40
|
+
orig_sysread(size)
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_string
|
44
|
+
response_io = StringIO.new
|
45
|
+
@env = Rack::RequestStringParser.env(@written)
|
46
|
+
Rack::Handler::StringIO.run(app(@env), @env, response_io)
|
47
|
+
@response = Rack::Handler::StringIO.response
|
48
|
+
self.string = response_io.string
|
49
|
+
end
|
50
|
+
|
51
|
+
def app(env)
|
52
|
+
app_to_use = @map if !@map.respond_to? :mapping
|
53
|
+
unless app_to_use
|
54
|
+
path = env["PATH_INFO"].to_s.squeeze("/")
|
55
|
+
hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
|
56
|
+
@map.mapping.each { |host, location, app|
|
57
|
+
next unless (hHost == host || sName == host \
|
58
|
+
|| (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
|
59
|
+
next unless location == path[0, location.size]
|
60
|
+
next unless path[location.size] == nil || path[location.size] == ?/
|
61
|
+
|
62
|
+
env["SCRIPT_NAME"] += location
|
63
|
+
env["PATH_INFO"] = path[location.size..-1]
|
64
|
+
app_to_use = app
|
65
|
+
}
|
66
|
+
raise "No application associated with #{env['rack.url_scheme']}://#{hHost}#{path}" unless app_to_use
|
67
|
+
end
|
68
|
+
app_to_use
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module Rack
|
73
|
+
class URLMap
|
74
|
+
attr_reader :mapping
|
75
|
+
end
|
76
|
+
|
77
|
+
class RequestStringParser
|
78
|
+
def self.env_key(key)
|
79
|
+
key = key.split('-').join('_').upcase
|
80
|
+
key = "HTTP_#{key}" unless key =~ /content/i
|
81
|
+
key
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.env(input)
|
85
|
+
lines = input.split("\r\n")
|
86
|
+
|
87
|
+
# find blank line which seperates the headers from the body
|
88
|
+
index_of_blank = nil
|
89
|
+
lines.each_with_index{|e,i|
|
90
|
+
index_of_blank = i if e == ""
|
91
|
+
}
|
92
|
+
|
93
|
+
type, uri = lines.first.split(/\s+/)
|
94
|
+
|
95
|
+
if index_of_blank
|
96
|
+
headers = lines[1..index_of_blank]
|
97
|
+
body = lines[(index_of_blank + 1)..-1].first
|
98
|
+
else
|
99
|
+
headers = lines[1..-1]
|
100
|
+
end
|
101
|
+
|
102
|
+
headers = headers.inject({}){|h,e|
|
103
|
+
k,v = e.split(/:\s+/)
|
104
|
+
h.merge! env_key(k) => v if k
|
105
|
+
h
|
106
|
+
}
|
107
|
+
|
108
|
+
uri = URI(uri)
|
109
|
+
env = headers.dup
|
110
|
+
env["SERVER_NAME"] = uri.host
|
111
|
+
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
|
112
|
+
env["QUERY_STRING"] = uri.query.to_s
|
113
|
+
env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
|
114
|
+
env["SCRIPT_NAME"] = ""
|
115
|
+
env["REQUEST_METHOD"] = type
|
116
|
+
env["rack.url_scheme"] = uri.scheme || "http"
|
117
|
+
env["rack.input"] = body
|
118
|
+
env
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
module Handler
|
123
|
+
class StringIO
|
124
|
+
def self.response
|
125
|
+
@@response
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.run(app, env={}, output=StringIO.new)
|
129
|
+
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
130
|
+
env["QUERY_STRING"] ||= ""
|
131
|
+
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
132
|
+
env["REQUEST_PATH"] ||= "/"
|
133
|
+
|
134
|
+
status, headers, body = app.call(env)
|
135
|
+
@@response = Rack::Response.new(body, status, headers)
|
136
|
+
begin
|
137
|
+
send_headers output, status, headers
|
138
|
+
send_body output, body
|
139
|
+
output.rewind
|
140
|
+
ensure
|
141
|
+
body.close if body.respond_to? :close
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.send_headers(output, status, headers)
|
146
|
+
output.print "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}\r\n"
|
147
|
+
headers.each { |k, vs|
|
148
|
+
vs.each { |v|
|
149
|
+
output.print "#{k}: #{v}\r\n"
|
150
|
+
}
|
151
|
+
}
|
152
|
+
output.print "\r\n"
|
153
|
+
output.flush
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.send_body(output, body)
|
157
|
+
body.each { |part|
|
158
|
+
output.print part
|
159
|
+
output.flush
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mikehale-akismet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Hale
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-07 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: TODO
|
17
|
+
email: mikehale@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- VERSION.yml
|
26
|
+
- lib/akismet.rb
|
27
|
+
- spec/akismet_spec.rb
|
28
|
+
- spec/spec.opts
|
29
|
+
- spec/spec_helper.rb
|
30
|
+
- spec/spec_http.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://github.com/mikehale/akismet
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options:
|
35
|
+
- --inline-source
|
36
|
+
- --charset=UTF-8
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.2.0
|
55
|
+
signing_key:
|
56
|
+
specification_version: 2
|
57
|
+
summary: TODO
|
58
|
+
test_files: []
|
59
|
+
|