viking 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +1 -0
- data/lib/core_ext/object.rb +15 -0
- data/lib/core_ext/transformations.rb +35 -0
- data/lib/viking.rb +80 -0
- data/lib/viking/akismet.rb +203 -0
- data/lib/viking/base.rb +74 -0
- data/lib/viking/defensio.rb +384 -0
- data/lib/viking/version.rb +3 -0
- data/spec/core_ext/object_spec.rb +21 -0
- data/spec/core_ext/transformations_spec.rb +41 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/viking/akismet_spec.rb +99 -0
- data/spec/viking/base_spec.rb +47 -0
- data/spec/viking/defensio_spec.rb +147 -0
- data/spec/viking/viking_spec.rb +44 -0
- data/viking.gemspec +21 -0
- metadata +91 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
# = Defensio
|
6
|
+
# Adapted from code originally by Technoweenie. Updated to the 1.2 API, and
|
7
|
+
# refactored.
|
8
|
+
#
|
9
|
+
# = License
|
10
|
+
# Technoweenie fails to mention the license of his original code, so I assume
|
11
|
+
# that it is either under MIT or public domain. As such, I release this code
|
12
|
+
# under the MIT license.
|
13
|
+
#
|
14
|
+
# Copyright (c) 2008, James Herdman
|
15
|
+
#
|
16
|
+
# = Important Note
|
17
|
+
# * most documentation below is adapted from the Defensio API (v 1.2) manual
|
18
|
+
# * unless otherwise stated, all arguments are expected to be Strings
|
19
|
+
module Viking
|
20
|
+
class Defensio < Base
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
attr_accessor :host
|
25
|
+
attr_accessor :port
|
26
|
+
attr_accessor :api_version
|
27
|
+
attr_accessor :standard_headers
|
28
|
+
attr_accessor :service_type
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_accessor :proxy_port
|
33
|
+
attr_accessor :proxy_host
|
34
|
+
attr_reader :last_response
|
35
|
+
|
36
|
+
self.service_type = :blog
|
37
|
+
self.host = 'api.defensio.com'
|
38
|
+
self.api_version = '1.2'
|
39
|
+
self.standard_headers = {
|
40
|
+
'User-Agent' => "Viking (Ruby Gem) v#{Viking::VERSION}",
|
41
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
42
|
+
}
|
43
|
+
|
44
|
+
# Create a new instance of the Akismet class
|
45
|
+
#
|
46
|
+
# ==== Arguments
|
47
|
+
# Arguments are provided in the form of a Hash with the following keys
|
48
|
+
# (as Symbols) available:
|
49
|
+
#
|
50
|
+
# +api_key+:: your Defensio API key
|
51
|
+
# +blog+:: the blog associated with your api key
|
52
|
+
#
|
53
|
+
# The following keys are available and are entirely optional. They are
|
54
|
+
# available incase communication with Akismet's servers requires a
|
55
|
+
# proxy port and/or host:
|
56
|
+
#
|
57
|
+
# * +proxy_port+
|
58
|
+
# * +proxy_host+
|
59
|
+
def initialize(options)
|
60
|
+
super
|
61
|
+
self.verify_options = false
|
62
|
+
end
|
63
|
+
|
64
|
+
# This action verifies that the key is valid for the owner calling the
|
65
|
+
# service. A user must have a valid API key in order to use Defensio.
|
66
|
+
#
|
67
|
+
# ==== Returns
|
68
|
+
# true, false
|
69
|
+
def verified?
|
70
|
+
return false if invalid_options?
|
71
|
+
(@verify_options ||= call_defensio('validate-key'))[:status] == 'success'
|
72
|
+
end
|
73
|
+
|
74
|
+
# This action should be invoked upon the publication of an article to
|
75
|
+
# announce its existence. The actual content of the article is sent to
|
76
|
+
# Defensio for analysis.
|
77
|
+
#
|
78
|
+
# ==== Arguments
|
79
|
+
# Provided in a Hash with the following keys:
|
80
|
+
#
|
81
|
+
# +owner_url+ (*required*)::
|
82
|
+
# the URL of the owner using Defensio. Note that this value should be
|
83
|
+
# provided in your default options for Viking and will be automatically
|
84
|
+
# inserted into your call.
|
85
|
+
# +article_author+ (*required*)::
|
86
|
+
# the name of the author of the article
|
87
|
+
# +article_author_email+ (*required*)::
|
88
|
+
# the email address of the article's author
|
89
|
+
# +article_title+ (*required*)::
|
90
|
+
# the title of the article
|
91
|
+
# +article_content+ (*required*)::
|
92
|
+
# the contents of the article
|
93
|
+
# +permalink+ (*required*)::
|
94
|
+
# the permalink of the article
|
95
|
+
#
|
96
|
+
# ==== Returns
|
97
|
+
# Hash::
|
98
|
+
# contains server response. Should things go awry, the full response
|
99
|
+
# will be provided.
|
100
|
+
#
|
101
|
+
# ===== Response structure
|
102
|
+
# The following are the fields returned from the server and their possible
|
103
|
+
# values:
|
104
|
+
#
|
105
|
+
# +status+::
|
106
|
+
# indicates whether or not the key is valid for this blog. Either
|
107
|
+
# 'success' or 'fail'.
|
108
|
+
# +message+::
|
109
|
+
# the message provided by the action, if applicable
|
110
|
+
# +api_version+::
|
111
|
+
# the API version used to process the request
|
112
|
+
def check_article(options = {})
|
113
|
+
return false if invalid_options?
|
114
|
+
call_defensio 'announce-article', options
|
115
|
+
end
|
116
|
+
|
117
|
+
# This central action determines not only whether Defensio thinks a
|
118
|
+
# comment is spam or not, but also a measure of its "spaminess", i.e. its
|
119
|
+
# relative likelihood of being spam.
|
120
|
+
#
|
121
|
+
# It should be noted that one of Defensio's key features is its ability to
|
122
|
+
# rank spam according to how "spammy" it appears to be. In order to make
|
123
|
+
# the most of the Defensio system in their applications, developers should
|
124
|
+
# take advantage of the spaminess value returned by this function, to
|
125
|
+
# build interfaces that make it easy for the user to quickly sort through
|
126
|
+
# and manage their spamboxes.
|
127
|
+
#
|
128
|
+
# ==== Arguments
|
129
|
+
# Provide arguments in a Hash. The following keys are available:
|
130
|
+
#
|
131
|
+
# +owner_url+ (*required*)::
|
132
|
+
# the URL of the owner using Defensio. Note that this value should be
|
133
|
+
# provided in your default options for Viking and will be automatically
|
134
|
+
# inserted into your call.
|
135
|
+
# +user_ip+ (*required*)::
|
136
|
+
# the IP address of whomever is posting the comment
|
137
|
+
# +article_date+ <~strftime> (*required*)::
|
138
|
+
# the date the original blog article was posted
|
139
|
+
# +comment_author+ (*required*)::
|
140
|
+
# the name of the author of the comment
|
141
|
+
# +comment_type+ (*required*)::
|
142
|
+
# the type of comment being posted to the article. This is expected to
|
143
|
+
# be any of the following: 'comment', 'trackback', 'pingback', 'other'
|
144
|
+
# +comment_content+::
|
145
|
+
# the content of the comment in question
|
146
|
+
# +comment_author_email+::
|
147
|
+
# the email address of the comment's author
|
148
|
+
# +permalink+::
|
149
|
+
# the permalink of the blog post to which the comment is being posted
|
150
|
+
# +referrer+::
|
151
|
+
# the URL of the site that brought the commenter to this page
|
152
|
+
# +user_logged_in+::
|
153
|
+
# whether or not the user leaving the comment is logged into the client
|
154
|
+
# platform. Expected to be either +true+ or +false+.
|
155
|
+
# +trusted_user+::
|
156
|
+
# whether or not the user is an administrator or modertor or editor of
|
157
|
+
# the blog. This should only ever be true if the blogging platform can
|
158
|
+
# guarentee that the user has been authenticated and authorized for this
|
159
|
+
# role. This is expected to be either +true+ or +false+.
|
160
|
+
# +openid+::
|
161
|
+
# the OpenID URL of the currently logged in user. Must be used in
|
162
|
+
# conjunction with +user_logged_in+ as +true+. OpenID authentication
|
163
|
+
# must be taken care of by your application.
|
164
|
+
# +test_force+::
|
165
|
+
# <b>FOR TESTING PURPOSES ONLY</b>: use this parameter to force the
|
166
|
+
# outcome of +audit_comment+. Optionally affix (with a comma) a desired
|
167
|
+
# +spaminess+ return value (in the range 0 to 1) (e.g. "spam,x.xxxx"
|
168
|
+
# "ham,x.xxxx" )
|
169
|
+
#
|
170
|
+
# ==== Returns
|
171
|
+
# Hash::
|
172
|
+
# contains server response. Should things go awry, the full response
|
173
|
+
# will be provided.
|
174
|
+
#
|
175
|
+
# ===== Response structure
|
176
|
+
# The following are the fields returned from the server and their possible
|
177
|
+
# values:
|
178
|
+
#
|
179
|
+
# +status+::
|
180
|
+
# indicates whether or not the key is valid for this blog. Either
|
181
|
+
# 'success' or 'fail'.
|
182
|
+
# +message+::
|
183
|
+
# the message provided by the action, if applicable
|
184
|
+
# +api_version+::
|
185
|
+
# the API version used to process the request
|
186
|
+
# +signature+::
|
187
|
+
# this uniquely identifies a message in the Defensio system. This should
|
188
|
+
# be retained by the client for retraining purposes.
|
189
|
+
# +spam+::
|
190
|
+
# whether or not Defensio believes the comment to be spam. This will be
|
191
|
+
# either +true+ or +false+
|
192
|
+
# +spaminess+::
|
193
|
+
# a value indicating the relative likelihood that a comment is spam.
|
194
|
+
# This should be retained to aid in building spam sorting interfaces.
|
195
|
+
def check_comment(options = {})
|
196
|
+
return false if invalid_options?
|
197
|
+
options[:article_date] = options[:article_date].strftime("%Y/%m/%d") # e.g. 2007/05/16
|
198
|
+
call_defensio 'audit-comment', options
|
199
|
+
end
|
200
|
+
|
201
|
+
# This action is used to retrain false negatives. That is to say, to
|
202
|
+
# indicate to the filter that comments originally tagged as "ham" (i.e.
|
203
|
+
# legitimate) were in fact spam.
|
204
|
+
#
|
205
|
+
# Retraining the filter in this manner contributes to a personalized
|
206
|
+
# learning effect on the filtering algorithm that will improve accuracy
|
207
|
+
# for each user over time.
|
208
|
+
#
|
209
|
+
# ==== Arguments
|
210
|
+
# Provide arguments in a Hash. The following keys are available:
|
211
|
+
#
|
212
|
+
# +owner_url+ (*required*)::
|
213
|
+
# the URL of the owner using Defensio. Note that this value should be
|
214
|
+
# provided in your default options for Viking and will be automatically
|
215
|
+
# inserted into your call.
|
216
|
+
# +signatures+ (comma separated Strings)(*required*)::
|
217
|
+
# a comma separated list of signatures (or single entry) to be submitted
|
218
|
+
# for retraining. The signatures were provided by Defensio when a
|
219
|
+
# comment was first audited.
|
220
|
+
#
|
221
|
+
# ==== Returns
|
222
|
+
# Hash::
|
223
|
+
# contains server response. Should things go awry, the full response
|
224
|
+
# will be provided.
|
225
|
+
#
|
226
|
+
# ===== Response structure
|
227
|
+
# The following are the fields returned from the server and their possible
|
228
|
+
# values:
|
229
|
+
#
|
230
|
+
# +status+::
|
231
|
+
# indicates whether or not the key is valid for this blog. Either
|
232
|
+
# 'success' or 'fail'.
|
233
|
+
# +message+::
|
234
|
+
# the message provided by the action, if applicable
|
235
|
+
# +api_version+::
|
236
|
+
# the API version used to process the request
|
237
|
+
def mark_as_spam(options = {})
|
238
|
+
return false if invalid_options?
|
239
|
+
call_defensio 'report-false-negatives', options
|
240
|
+
end
|
241
|
+
|
242
|
+
# This action is used to retrain false positives. That is to say, to
|
243
|
+
# indicate to the filter that comments originally tagged as spam were in
|
244
|
+
# fact "ham" (i.e. legitimate comments).
|
245
|
+
#
|
246
|
+
# Retraining the filter in this manner contributes to a personalized
|
247
|
+
# learning effect on the filtering algorithm that will improve accuracy
|
248
|
+
# for each user over time.
|
249
|
+
#
|
250
|
+
# ==== Arguments
|
251
|
+
# Provide arguments in a Hash. The following keys are available:
|
252
|
+
#
|
253
|
+
# +owner_url+ (*required*)::
|
254
|
+
# the URL of the owner using Defensio. Note that this value should be
|
255
|
+
# provided in your default options for Viking and will be automatically
|
256
|
+
# inserted into your call.
|
257
|
+
# +signatures+ (comma separated Strings)(*required*)::
|
258
|
+
# a comma separated list of signatures (or single entry) to be submitted
|
259
|
+
# for retraining. The signatures were provided by Defensio when a
|
260
|
+
# comment was first audited.
|
261
|
+
#
|
262
|
+
# ==== Returns
|
263
|
+
# Hash::
|
264
|
+
# contains server response. Should things go awry, the full response
|
265
|
+
# will be provided.
|
266
|
+
#
|
267
|
+
# ===== Response structure
|
268
|
+
# The following are the fields returned from the server and their possible
|
269
|
+
# values:
|
270
|
+
#
|
271
|
+
# +status+::
|
272
|
+
# indicates whether or not the key is valid for this blog. Either
|
273
|
+
# 'success' or 'fail'.
|
274
|
+
# +message+::
|
275
|
+
# the message provided by the action, if applicable
|
276
|
+
# +api_version+::
|
277
|
+
# the API version used to process the request
|
278
|
+
def mark_as_ham(options = {})
|
279
|
+
return false if invalid_options?
|
280
|
+
call_defensio 'report-false-positives', options
|
281
|
+
end
|
282
|
+
|
283
|
+
# This action returns basic statistics regarding the performance of
|
284
|
+
# Defensio since activation
|
285
|
+
#
|
286
|
+
# ==== Returns
|
287
|
+
# Hash::
|
288
|
+
# contains server response. Should things go awry, the full response
|
289
|
+
# will be provided.
|
290
|
+
#
|
291
|
+
# ===== Response structure
|
292
|
+
# The following are the fields returned from the server and their possible
|
293
|
+
# values:
|
294
|
+
#
|
295
|
+
# +status+::
|
296
|
+
# indicates whether or not the key is valid for this blog. Either
|
297
|
+
# 'success' or 'fail'.
|
298
|
+
# +message+::
|
299
|
+
# the message provided by the action, if applicable
|
300
|
+
# +api_version+::
|
301
|
+
# the API version used to process the request
|
302
|
+
# +accuracy+::
|
303
|
+
# a value between 0 and 1 representing the percentage of comments
|
304
|
+
# correctly identified as spam or ham by Defensio on this blog
|
305
|
+
# +spam+::
|
306
|
+
# the number of spam comments caught by the filter
|
307
|
+
# +ham+::
|
308
|
+
# the number of legitimate comments caught by the filter
|
309
|
+
# +false_positives+::
|
310
|
+
# the number of times legitimate messages have been retrained (i.e.
|
311
|
+
# "de-spammed") by the user
|
312
|
+
# +false_negatives+::
|
313
|
+
# the number of times a comments had to be marked as spam by the user
|
314
|
+
# +learning+::
|
315
|
+
# whether or not Defensio is still in its initial learning phase (either
|
316
|
+
# +true+ or +false+)
|
317
|
+
# +learning_status+::
|
318
|
+
# more reasons on why Defensio is still learning
|
319
|
+
def stats
|
320
|
+
return false if invalid_options?
|
321
|
+
call_defensio 'get-stats'
|
322
|
+
end
|
323
|
+
|
324
|
+
# Formats a URL for use with the Defensio service.
|
325
|
+
#
|
326
|
+
# ==== Arguments
|
327
|
+
# +action+ <String>:: the action you wish to call
|
328
|
+
#
|
329
|
+
# ==== Returns
|
330
|
+
# String
|
331
|
+
#
|
332
|
+
# ==== Example
|
333
|
+
# > defensio.url('get-stats')
|
334
|
+
# => '/blog/1.2/get-stats/1234abc.yaml'
|
335
|
+
def url(action)
|
336
|
+
URI.escape(
|
337
|
+
[
|
338
|
+
'', # ensures opening /
|
339
|
+
self.class.service_type,
|
340
|
+
self.class.api_version,
|
341
|
+
action,
|
342
|
+
self.options[:api_key]
|
343
|
+
].join('/')
|
344
|
+
) << '.yaml'
|
345
|
+
end
|
346
|
+
|
347
|
+
protected
|
348
|
+
|
349
|
+
def call_defensio(action, params = {})
|
350
|
+
resp = defensio_http.post(
|
351
|
+
url(action),
|
352
|
+
data(params),
|
353
|
+
self.class.standard_headers
|
354
|
+
)
|
355
|
+
log_request(url(action), data, resp)
|
356
|
+
process_response_body(resp.body)
|
357
|
+
end
|
358
|
+
|
359
|
+
def defensio_http
|
360
|
+
Net::HTTP.new(
|
361
|
+
self.class.host,
|
362
|
+
self.class.port,
|
363
|
+
options[:proxy_host],
|
364
|
+
options[:proxy_port]
|
365
|
+
)
|
366
|
+
end
|
367
|
+
|
368
|
+
def data(params = {})
|
369
|
+
params.update('owner-url' => self.options[:blog] || options[:owner_url]).dasherize_keys.to_query
|
370
|
+
end
|
371
|
+
|
372
|
+
private
|
373
|
+
|
374
|
+
attr_accessor :verify_options
|
375
|
+
|
376
|
+
def process_response_body(response_body)
|
377
|
+
data = YAML.load(response_body)
|
378
|
+
return data['defensio-result'].symbolize_keys
|
379
|
+
rescue
|
380
|
+
{ :data => data, :status => 'fail' }
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Object do
|
4
|
+
|
5
|
+
describe '#to_param' do
|
6
|
+
|
7
|
+
it 'should return a representation of an object' do
|
8
|
+
1.to_param.should == '1'
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#to_query' do
|
14
|
+
|
15
|
+
it 'should CGI escape an object and its associated key' do
|
16
|
+
'foo'.to_query('bar').should == 'bar=foo'
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hash do
|
4
|
+
|
5
|
+
describe '#symbolize_keys' do
|
6
|
+
|
7
|
+
it 'should convert all keys to symbols' do
|
8
|
+
{ 'foo' => 'bar', :baz => 1 }.symbolize_keys.should == { :foo => 'bar', :baz => 1 }
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should handle bad keys' do
|
12
|
+
{ nil => 'bar' }.symbolize_keys[nil].should == 'bar'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#dasherize_keys' do
|
18
|
+
|
19
|
+
it 'should convert all all underscores in keys to dashes' do
|
20
|
+
{ 'foo_bar' => 'baz' }.dasherize_keys.should == { 'foo-bar' => 'baz' }
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#to_query' do
|
26
|
+
|
27
|
+
it 'should convert to a valid URI query' do
|
28
|
+
{ :foo => 'baz', :bar => 1 }.to_query.should == 'bar=1&foo=baz'
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
describe Array, '#to_query' do
|
36
|
+
|
37
|
+
it 'should convert to a valid URI query' do
|
38
|
+
[:foo, :bar].to_query('baz').should == 'baz%5B%5D=foo&baz%5B%5D=bar'
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|