scamp 1.2.0 → 2.0.0.pre
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.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/README.md +41 -128
- data/examples/v2.rb +39 -0
- data/lib/scamp/adapter.rb +47 -0
- data/lib/scamp/matcher.rb +15 -91
- data/lib/scamp/matches.rb +22 -0
- data/lib/scamp/message.rb +39 -0
- data/lib/scamp/plugin.rb +33 -0
- data/lib/scamp/version.rb +1 -1
- data/lib/scamp.rb +33 -43
- data/scamp.gemspec +9 -10
- data/spec/lib/adapter_spec.rb +145 -0
- data/spec/lib/matcher_spec.rb +122 -0
- data/spec/lib/matches_spec.rb +29 -0
- data/spec/lib/message_spec.rb +52 -0
- data/spec/lib/plugin_spec.rb +33 -0
- data/spec/lib/scamp_spec.rb +119 -659
- data/spec/spec_helper.rb +23 -1
- metadata +55 -58
- data/examples/bot.rb +0 -72
- data/lib/scamp/action.rb +0 -70
- data/lib/scamp/connection.rb +0 -32
- data/lib/scamp/messages.rb +0 -35
- data/lib/scamp/rooms.rb +0 -116
- data/lib/scamp/users.rb +0 -51
data/lib/scamp.rb
CHANGED
@@ -1,31 +1,16 @@
|
|
1
1
|
require 'eventmachine'
|
2
|
-
require 'em-http-request'
|
3
|
-
require 'yajl'
|
4
2
|
require "logger"
|
5
3
|
|
6
4
|
require "scamp/version"
|
7
|
-
require 'scamp/connection'
|
8
|
-
require 'scamp/rooms'
|
9
|
-
require 'scamp/users'
|
10
5
|
require 'scamp/matcher'
|
11
|
-
require 'scamp/
|
12
|
-
require 'scamp/
|
6
|
+
require 'scamp/adapter'
|
7
|
+
require 'scamp/plugin'
|
13
8
|
|
14
9
|
class Scamp
|
15
|
-
|
16
|
-
include Rooms
|
17
|
-
include Users
|
18
|
-
include Messages
|
10
|
+
attr_accessor :adapters, :plugins, :matchers, :logger, :verbose, :first_match_only, :required_format, :strip_prefix
|
19
11
|
|
20
|
-
|
21
|
-
:logger, :verbose, :first_match_only, :ignore_self, :required_prefix,
|
22
|
-
:rooms_to_join
|
23
|
-
|
24
|
-
def initialize(options = {})
|
12
|
+
def initialize(options = {}, &block)
|
25
13
|
options ||= {}
|
26
|
-
raise ArgumentError, "You must pass an API key" unless options[:api_key]
|
27
|
-
raise ArgumentError, "You must pass a subdomain" unless options[:subdomain]
|
28
|
-
|
29
14
|
options.each do |k,v|
|
30
15
|
s = "#{k}="
|
31
16
|
if respond_to?(s)
|
@@ -34,31 +19,41 @@ class Scamp
|
|
34
19
|
logger.warn "Scamp initialized with #{k.inspect} => #{v.inspect} but NO UNDERSTAND!"
|
35
20
|
end
|
36
21
|
end
|
37
|
-
|
38
|
-
@
|
39
|
-
@rooms = {}
|
40
|
-
@user_cache = {}
|
41
|
-
@room_cache = {}
|
22
|
+
|
23
|
+
@strip_prefix ||= true
|
42
24
|
@matchers ||= []
|
25
|
+
@adapters ||= {}
|
26
|
+
@plugins ||= []
|
27
|
+
|
28
|
+
yield self
|
43
29
|
end
|
44
|
-
|
45
|
-
def
|
46
|
-
|
30
|
+
|
31
|
+
def adapter name, klass, opts={}
|
32
|
+
adapter = klass.new self, opts
|
33
|
+
sid = adapter.subscribe do |context, msg|
|
34
|
+
process_message(name, context, msg)
|
35
|
+
end
|
36
|
+
@adapters[name] = {:adapter => adapter, :sid => sid}
|
47
37
|
end
|
48
|
-
|
49
|
-
def
|
50
|
-
|
51
|
-
connect(api_key, room_list, &blk)
|
38
|
+
|
39
|
+
def plugin klass, opts={}
|
40
|
+
plugins << klass.new(self, opts)
|
52
41
|
end
|
53
|
-
|
54
|
-
def
|
55
|
-
|
42
|
+
|
43
|
+
def connect!
|
44
|
+
EM.run do
|
45
|
+
@adapters.each do |name, data|
|
46
|
+
logger.info "Connecting to #{name} adapter"
|
47
|
+
data[:adapter].connect!
|
48
|
+
end
|
49
|
+
end
|
56
50
|
end
|
57
51
|
|
58
52
|
def logger
|
59
53
|
unless @logger
|
60
54
|
@logger = Logger.new(STDOUT)
|
61
55
|
@logger.level = (verbose ? Logger::DEBUG : Logger::INFO)
|
56
|
+
@logger.info "Scamp using default logger as none was provided"
|
62
57
|
end
|
63
58
|
@logger
|
64
59
|
end
|
@@ -73,18 +68,13 @@ class Scamp
|
|
73
68
|
@first_match_only
|
74
69
|
end
|
75
70
|
|
76
|
-
private
|
77
|
-
|
78
71
|
def match trigger, params={}, &block
|
79
|
-
params
|
80
|
-
matchers << Matcher.new(self, {:trigger => trigger, :action => block, :conditions => params[:conditions], :required_prefix => required_prefix})
|
72
|
+
matchers << Matcher.new(self, {:trigger => trigger, :action => block, :on => params[:on], :conditions => params[:conditions]})
|
81
73
|
end
|
82
|
-
|
83
|
-
def process_message(msg)
|
84
|
-
logger.debug "Received message #{msg.inspect}"
|
85
|
-
return false if ignore_self && is_me?(msg[:user_id])
|
74
|
+
|
75
|
+
def process_message(channel, context, msg)
|
86
76
|
matchers.each do |matcher|
|
87
|
-
break if first_match_only & matcher.attempt(msg)
|
77
|
+
break if first_match_only & matcher.attempt(channel, context, msg)
|
88
78
|
end
|
89
79
|
end
|
90
80
|
end
|
data/scamp.gemspec
CHANGED
@@ -5,8 +5,8 @@ require "scamp/version"
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "scamp"
|
7
7
|
s.version = Scamp::VERSION
|
8
|
-
s.authors = ["Will Jessop"]
|
9
|
-
s.email = ["will@willj.net"]
|
8
|
+
s.authors = ["Will Jessop", "Adam Holt"]
|
9
|
+
s.email = ["will@willj.net", "me@adamholt.co.uk"]
|
10
10
|
s.homepage = "https://github.com/wjessop/Scamp"
|
11
11
|
s.summary = %q{Eventmachine based Campfire bot framework}
|
12
12
|
s.description = %q{Eventmachine based Campfire bot framework}
|
@@ -15,13 +15,12 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
16
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
17
|
s.require_paths = ["lib"]
|
18
|
-
|
19
|
-
s.add_dependency('eventmachine', '~> 1.0.0.rc.4')
|
20
|
-
s.add_dependency('yajl-ruby', '~> 1.1')
|
21
|
-
s.add_dependency('em-http-request', '~> 1.0')
|
22
18
|
|
23
|
-
s.
|
24
|
-
|
25
|
-
s.add_development_dependency "
|
26
|
-
s.add_development_dependency "
|
19
|
+
s.add_dependency 'eventmachine', '~> 1.0'
|
20
|
+
|
21
|
+
s.add_development_dependency "rake"
|
22
|
+
s.add_development_dependency "rspec", '~> 2.11'
|
23
|
+
s.add_development_dependency "mocha", '~> 0.12'
|
24
|
+
s.add_development_dependency "webmock", '~> 1.8'
|
25
|
+
s.add_development_dependency "simplecov"
|
27
26
|
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Scamp::Adapter do
|
4
|
+
let(:bot) { stub }
|
5
|
+
describe "#new" do
|
6
|
+
it "creates a new adapter" do
|
7
|
+
adapter = Scamp::Adapter.new(bot)
|
8
|
+
adapter.should_not be_nil
|
9
|
+
end
|
10
|
+
|
11
|
+
it "accepts a hash of options" do
|
12
|
+
adapter = Scamp::Adapter.new(bot, :hello => "world")
|
13
|
+
adapter.should_not be_nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".matches_required_format?" do
|
18
|
+
let(:adapter) { Scamp::Adapter.new(bot) }
|
19
|
+
let(:bot) { stub(:required_format => nil) }
|
20
|
+
|
21
|
+
describe "no requirement" do
|
22
|
+
it "matches required format" do
|
23
|
+
adapter.matches_required_format?("Hello").should be_true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "string requirement" do
|
28
|
+
let(:bot) { stub(:required_format => "Bot: ") }
|
29
|
+
|
30
|
+
it "matches msg with correct prefix" do
|
31
|
+
adapter.matches_required_format?("Bot: Hello").should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it "does not match msg with incorrect prefix" do
|
35
|
+
adapter.matches_required_format?("Not: Hello").should be_false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "regex requirement" do
|
40
|
+
let(:bot) { stub(:required_format => /^Bot: /) }
|
41
|
+
|
42
|
+
it "matches msg with correct prefix" do
|
43
|
+
adapter.matches_required_format?("Bot: Hello").should be_true
|
44
|
+
end
|
45
|
+
|
46
|
+
it "does not match msg with incorrect prefix" do
|
47
|
+
adapter.matches_required_format?("Not: Hello").should be_false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "other requirement" do
|
52
|
+
let(:bot) { stub(:required_format => Object.new) }
|
53
|
+
|
54
|
+
it "raises an ArgumentError" do
|
55
|
+
expect {
|
56
|
+
adapter.matches_required_format?("Bot: Hello").should be_true
|
57
|
+
}.to raise_error(ArgumentError)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe ".strip_prefix" do
|
63
|
+
describe "with strip_prefix enabled" do
|
64
|
+
describe "with a string require_format" do
|
65
|
+
let(:adapter) { Scamp::Adapter.new(bot) }
|
66
|
+
let(:bot) { stub(:required_format => "Bot: ", :strip_prefix => true) }
|
67
|
+
|
68
|
+
it "should strip the required_format if it is a string" do
|
69
|
+
adapter.strip_prefix("Bot: Hello").should == "Hello"
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should not strip an incorrect prefix" do
|
73
|
+
adapter.strip_prefix("Not: Hello").should == "Not: Hello"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "with a Regex require_format" do
|
78
|
+
let(:adapter) { Scamp::Adapter.new(bot) }
|
79
|
+
let(:bot) { stub(:required_format => /^Bot: /, :strip_prefix => true) }
|
80
|
+
|
81
|
+
it "should strip the required_format if it is a string" do
|
82
|
+
adapter.strip_prefix("Bot: Hello").should == "Bot: Hello"
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should not strip an incorrect prefix" do
|
86
|
+
adapter.strip_prefix("Not: Hello").should == "Not: Hello"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "with strip_prefix disabled" do
|
92
|
+
let(:adapter) { Scamp::Adapter.new(bot) }
|
93
|
+
let(:bot) { stub(:required_format => "Bot: ", :strip_prefix => false) }
|
94
|
+
|
95
|
+
it "should strip the required_format if it is a string" do
|
96
|
+
adapter.strip_prefix("Bot: Hello").should == "Bot: Hello"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should not strip an incorrect prefix" do
|
100
|
+
adapter.strip_prefix("Not: Hello").should == "Not: Hello"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "channels" do
|
106
|
+
let(:adapter) { Scamp::Adapter.new(bot) }
|
107
|
+
|
108
|
+
describe ".push" do
|
109
|
+
it "should push through the correct value" do
|
110
|
+
EM.run_block do
|
111
|
+
value = nil
|
112
|
+
adapter.subscribe do |msg|
|
113
|
+
value = msg
|
114
|
+
end
|
115
|
+
adapter.push "Hello World"
|
116
|
+
|
117
|
+
value.should == "Hello World"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe ".subscribe" do
|
123
|
+
it "allows subscriptions" do
|
124
|
+
EM.run_block do
|
125
|
+
subscribed = false
|
126
|
+
adapter.subscribe do |msg|
|
127
|
+
subscribed = true
|
128
|
+
end
|
129
|
+
adapter.push true
|
130
|
+
|
131
|
+
subscribed.should be_true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe ".connect!" do
|
138
|
+
let(:adapter) { Scamp::Adapter.new(bot) }
|
139
|
+
it "should raise an implementation error" do
|
140
|
+
expect {
|
141
|
+
adapter.connect!
|
142
|
+
}.to raise_error(NotImplementedError)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Scamp::Matcher do
|
4
|
+
describe "#new" do
|
5
|
+
let(:bot) { stub(adapters: {one: "two"}) }
|
6
|
+
|
7
|
+
it "requires a trigger" do
|
8
|
+
expect {
|
9
|
+
Scamp::Matcher.new(bot, action: ->(ctx, msg){})
|
10
|
+
}.to raise_error(ArgumentError)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "requires a action" do
|
14
|
+
expect {
|
15
|
+
Scamp::Matcher.new(bot, trigger: "Hello")
|
16
|
+
}.to raise_error(ArgumentError)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe ".attempt" do
|
21
|
+
context "channels" do
|
22
|
+
let(:msg) { Scamp::Message.new stub, body: "hello" }
|
23
|
+
|
24
|
+
context "listened" do
|
25
|
+
it "runs the action when" do
|
26
|
+
result = false
|
27
|
+
matcher = Scamp::Matcher.new stub,
|
28
|
+
on: [:one],
|
29
|
+
trigger: "hello",
|
30
|
+
action: ->(ctx, msg){result = true}
|
31
|
+
matcher.attempt(:one, stub, msg)
|
32
|
+
result.should be_true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "unlistened" do
|
37
|
+
it "does not run the action" do
|
38
|
+
result = false
|
39
|
+
matcher = Scamp::Matcher.new stub,
|
40
|
+
on: [:one],
|
41
|
+
trigger: "hello",
|
42
|
+
action: ->(ctx, msg){result = true}
|
43
|
+
matcher.attempt(:two, stub, msg)
|
44
|
+
result.should be_false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "triggers" do
|
50
|
+
context "matching" do
|
51
|
+
it "runs the action" do
|
52
|
+
result = false
|
53
|
+
matcher = Scamp::Matcher.new stub,
|
54
|
+
on: [:one],
|
55
|
+
trigger: "hello",
|
56
|
+
action: ->(ctx, msg){result = true}
|
57
|
+
msg = Scamp::Message.new stub, body: "hello"
|
58
|
+
|
59
|
+
matcher.attempt(:one, stub, msg)
|
60
|
+
result.should be_true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "non matching" do
|
65
|
+
it "runs the action" do
|
66
|
+
result = false
|
67
|
+
matcher = Scamp::Matcher.new stub,
|
68
|
+
on: [:one],
|
69
|
+
trigger: "hello",
|
70
|
+
action: ->(ctx, msg){result = true}
|
71
|
+
msg = Scamp::Message.new stub, body: "goodbye"
|
72
|
+
|
73
|
+
matcher.attempt(:one, stub, msg)
|
74
|
+
result.should be_false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "validations" do
|
80
|
+
let(:message_class) {
|
81
|
+
class MyMessage < Scamp::Message
|
82
|
+
def valid?(conditions)
|
83
|
+
conditions[:hello] == "world"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
MyMessage
|
87
|
+
}
|
88
|
+
|
89
|
+
context "valid message" do
|
90
|
+
it "runs the action" do
|
91
|
+
result = false
|
92
|
+
matcher = Scamp::Matcher.new stub,
|
93
|
+
on: [:one],
|
94
|
+
trigger: "hello",
|
95
|
+
action: ->(ctx, msg){result = true},
|
96
|
+
conditions: {hello: "world"}
|
97
|
+
|
98
|
+
msg = message_class.new stub, body: "hello"
|
99
|
+
|
100
|
+
matcher.attempt(:one, stub, msg)
|
101
|
+
result.should be_true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "invalid message" do
|
106
|
+
it "runs the action" do
|
107
|
+
result = false
|
108
|
+
matcher = Scamp::Matcher.new stub,
|
109
|
+
on: [:one],
|
110
|
+
trigger: "hello",
|
111
|
+
action: ->(ctx, msg){result = true},
|
112
|
+
conditions: {hello: "goodbye"}
|
113
|
+
|
114
|
+
msg = message_class.new stub, body: "hello"
|
115
|
+
|
116
|
+
matcher.attempt(:one, stub, msg)
|
117
|
+
result.should be_false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Scamp::Matches do
|
4
|
+
let(:msg) { "Name: Scamp, URL: https://github.com/wjessop/Scamp/" }
|
5
|
+
let(:match) { msg.match(/^Name: (?<name>\w+), URL: (?<url>.*)/) }
|
6
|
+
let(:matches) { Scamp::Matches.new(match) }
|
7
|
+
|
8
|
+
it "exposes the name from the capture" do
|
9
|
+
matches.name.should eql("Scamp")
|
10
|
+
matches[0].should eql("Scamp")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "exposes the url from the capture" do
|
14
|
+
matches.url.should eql("https://github.com/wjessop/Scamp/")
|
15
|
+
matches[1].should eql("https://github.com/wjessop/Scamp/")
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.each' do
|
19
|
+
it "iterates over each match" do
|
20
|
+
results = []
|
21
|
+
matches.each do |match|
|
22
|
+
results << match
|
23
|
+
end
|
24
|
+
|
25
|
+
results.should include("Scamp")
|
26
|
+
results.should include("https://github.com/wjessop/Scamp/")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Scamp::Message do
|
4
|
+
describe "#new" do
|
5
|
+
it "should store and provide access to metadata" do
|
6
|
+
msg = Scamp::Message.new("foo", body: "hello", foo: "foo", bar: "bar")
|
7
|
+
msg.foo.should eql("foo")
|
8
|
+
msg.bar.should eql("bar")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should maintain metadata integrity when multiple instances exist" do
|
12
|
+
msg = Scamp::Message.new("foo", body: "hello", foo: "foo")
|
13
|
+
msg2 = Scamp::Message.new("foo", body: "hello", foo: "ouchie")
|
14
|
+
msg.foo.should eql("foo")
|
15
|
+
msg.foo.should eql("foo")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should isolate data access message instances created by different adapters" do
|
19
|
+
msg = Scamp::Message.new("foo", body: "hello", foo: "foo")
|
20
|
+
msg2 = Scamp::Message.new("foo2", body: "hello", bar: "bar")
|
21
|
+
|
22
|
+
msg.should respond_to(:foo)
|
23
|
+
msg.should_not respond_to(:bar)
|
24
|
+
|
25
|
+
msg2.should respond_to(:bar)
|
26
|
+
msg2.should_not respond_to(:foo)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ".valid?" do
|
31
|
+
it "returns true by default" do
|
32
|
+
msg = Scamp::Message.new('foo', body: "hello")
|
33
|
+
msg.valid?(nil).should be_true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe ".matches?" do
|
38
|
+
let(:msg) { Scamp::Message.new('foo', body: "hello") }
|
39
|
+
|
40
|
+
it "matches a string trigger" do
|
41
|
+
msg.matches?("hello").should be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "matches a regex trigger" do
|
45
|
+
msg.matches?(/^hello$/).should be_true
|
46
|
+
end
|
47
|
+
|
48
|
+
it "does not match an invalid matcher" do
|
49
|
+
msg.matches?("goodbye").should be_false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Scamp::Plugin do
|
4
|
+
describe "#matchers" do
|
5
|
+
it "is empty by default" do
|
6
|
+
Scamp::Plugin.matchers.should be_empty
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should add a new matcher" do
|
10
|
+
expect {
|
11
|
+
Scamp::Plugin.match "Hello", :method
|
12
|
+
}.to change { Scamp::Plugin.matchers.size }.by(1)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe ".new" do
|
16
|
+
let(:bot) { mock }
|
17
|
+
let(:plugin) {
|
18
|
+
class TestPlugin < Scamp::Plugin
|
19
|
+
match "Hello", :name
|
20
|
+
|
21
|
+
def name(context, msg)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
TestPlugin
|
25
|
+
}
|
26
|
+
|
27
|
+
it "attaches matchers to the bot" do
|
28
|
+
bot.expects(:match).with("Hello", {:on => nil}).once
|
29
|
+
plugin.new(bot)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|