viking 1.0.0
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/.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
|