tyler-uppercut 0.5.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/LICENSE +19 -0
- data/README.textile +103 -0
- data/Rakefile +71 -0
- data/examples/basic_agent.rb +21 -0
- data/lib/uppercut/agent.rb +145 -0
- data/lib/uppercut/base.rb +90 -0
- data/lib/uppercut/conversation.rb +28 -0
- data/lib/uppercut/message.rb +18 -0
- data/lib/uppercut/notifier.rb +32 -0
- data/lib/uppercut.rb +8 -0
- data/specs/agent_spec.rb +176 -0
- data/specs/conversation_spec.rb +14 -0
- data/specs/jabber_stub.rb +76 -0
- data/specs/notifier_spec.rb +85 -0
- data/specs/spec_helper.rb +45 -0
- metadata +75 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2008 Tyler McMullen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
h1. Uppercut
|
2
|
+
|
3
|
+
|
4
|
+
h2. Overview
|
5
|
+
|
6
|
+
Uppercut is a little DSL for writing agents and notifiers which you interact with via your Jabber client.
|
7
|
+
|
8
|
+
|
9
|
+
h2. Making an Agent
|
10
|
+
|
11
|
+
You could put together a very simple agent as follows:
|
12
|
+
|
13
|
+
<pre>
|
14
|
+
<code>
|
15
|
+
class BasicAgent < Uppercut::Agent
|
16
|
+
command 'date' do |c|
|
17
|
+
m.send `date`
|
18
|
+
end
|
19
|
+
|
20
|
+
command /^cat (.*)/ do |c,rest|
|
21
|
+
m.send File.read(rest)
|
22
|
+
end
|
23
|
+
|
24
|
+
command 'report' do |c|
|
25
|
+
m.send 'Hostname: ' + `hostname`
|
26
|
+
m.send 'Running as: ' + ENV['USER']
|
27
|
+
end
|
28
|
+
end
|
29
|
+
</code>
|
30
|
+
</pre>
|
31
|
+
|
32
|
+
With the above code, we've created an Agent template of sorts. It responds to three commands: {date cat report}.
|
33
|
+
The block which is passed to "command" should always have at least one paramater. The first parameter will
|
34
|
+
always be an Uppercut::Conversation object, which can be used to respond to the user who sent the input. When passing
|
35
|
+
a regular expression as the first parameter to "command", all of the _captures_ which the pattern match generates
|
36
|
+
will also be passed to the block. This can be seen in the "cat" example above. There is one capture and it is
|
37
|
+
passed in as _rest_.
|
38
|
+
|
39
|
+
Then to actually put the Agent to work...
|
40
|
+
|
41
|
+
<pre>
|
42
|
+
<code>
|
43
|
+
BasicAgent.new('user@server/resource','password').listen
|
44
|
+
</code>
|
45
|
+
</pre>
|
46
|
+
|
47
|
+
This creates a new BasicAgent instance and sends it _listen_ to make it start accepting messages.
|
48
|
+
Note that by default when an agent is listening, it ignores all errors. (In the future, I'll have it log them.)
|
49
|
+
|
50
|
+
|
51
|
+
h2. Making a Notifier
|
52
|
+
|
53
|
+
<pre>
|
54
|
+
<code>
|
55
|
+
class BasicNotifier < Uppercut::Notifier
|
56
|
+
notifier :error do |m,data|
|
57
|
+
m.to 'tbmcmullen@gmail.com'
|
58
|
+
m.send "Something in your app blew up: #{data}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
</code>
|
62
|
+
</pre>
|
63
|
+
|
64
|
+
So, we make a new Notifier class and call a _notifier_ block within it. This makes a notifier with the name :error, which sends a message to tbmcmullen@gmail.com when it fires.
|
65
|
+
|
66
|
+
<pre>
|
67
|
+
<code>
|
68
|
+
notifier = BasicNotifier.new('user@server/resource','password').connect
|
69
|
+
|
70
|
+
notifier.notify :error, 'Sprocket Error!'
|
71
|
+
</code>
|
72
|
+
</pre>
|
73
|
+
|
74
|
+
The purpose of a Notifier is just to make a multi-purpose event-driven notification system. You could use it notify yourself of errors in your Rails app, or ping you when a new comment arrives on your blog, or ... I dunno, something else.
|
75
|
+
|
76
|
+
I am very aware of how limited this system is at the moment. I have some big plans for Notifier, so just hang tight while I hack them out...
|
77
|
+
|
78
|
+
h2. Todo
|
79
|
+
|
80
|
+
|
81
|
+
h3. Security
|
82
|
+
|
83
|
+
If you intend to use this on an open Jabber network (read: Google Talk) take some precautions. Agent#new takes an optional list of JIDs that the agent will respond to.
|
84
|
+
|
85
|
+
<pre>
|
86
|
+
<code>
|
87
|
+
BasicAgent.new('user@server/res', 'pw', :roster => ['you@server'])
|
88
|
+
</code>
|
89
|
+
</pre>
|
90
|
+
|
91
|
+
The agent created with the above statement will not respond to messages or subscription requests from anyone other than 'you@server'.
|
92
|
+
|
93
|
+
h3. Features
|
94
|
+
|
95
|
+
Uppercut is currently very thin on features, as it's in its infancy. Here's a brief list of where I'm
|
96
|
+
currently intending to take the project:
|
97
|
+
|
98
|
+
* send files to and receive files from agents
|
99
|
+
* improving the way one writes executables (using the Daemons library is best for the moment)
|
100
|
+
* auto-updating of agents
|
101
|
+
* I swear I'm going to find a use for MUC
|
102
|
+
* allow agents to establish communications on their own, rather than being reactionary (think RSS updates)
|
103
|
+
* ... other stuff that sounds fun to code up ...
|
data/Rakefile
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# Based on the merb-core Rakefile. Thanks.
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "rake/clean"
|
5
|
+
require "rake/gempackagetask"
|
6
|
+
require "rake/rdoctask"
|
7
|
+
require "spec/rake/spectask"
|
8
|
+
require "fileutils"
|
9
|
+
|
10
|
+
require File.dirname(__FILE__) + "/lib/uppercut"
|
11
|
+
|
12
|
+
include FileUtils
|
13
|
+
|
14
|
+
NAME = "uppercut"
|
15
|
+
|
16
|
+
##############################################################################
|
17
|
+
# Packaging & Installation
|
18
|
+
##############################################################################
|
19
|
+
CLEAN.include ["**/.*.sw?", "pkg", "lib/*.bundle", "*.gem", "doc/rdoc", ".config", "coverage", "cache"]
|
20
|
+
|
21
|
+
windows = (PLATFORM =~ /win32|cygwin/) rescue nil
|
22
|
+
install_home = ENV['GEM_HOME'] ? "-i #{ENV['GEM_HOME']}" : ""
|
23
|
+
|
24
|
+
SUDO = windows ? "" : "sudo"
|
25
|
+
|
26
|
+
desc "Packages Uppercut."
|
27
|
+
task :default => :package
|
28
|
+
|
29
|
+
task :uppercut => [:clean, :rdoc, :package]
|
30
|
+
|
31
|
+
spec = eval(File.read(File.join(File.dirname(__FILE__), 'uppercut.gemspec')))
|
32
|
+
|
33
|
+
Rake::GemPackageTask.new(spec) do |package|
|
34
|
+
package.gem_spec = spec
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "Run :package and install the resulting .gem"
|
38
|
+
task :install => :package do
|
39
|
+
sh %{#{SUDO} gem install #{install_home} --local pkg/#{NAME}-#{Uppercut::VERSION}.gem --no-rdoc --no-ri}
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "Run :clean and uninstall the .gem"
|
43
|
+
task :uninstall => :clean do
|
44
|
+
sh %{#{SUDO} gem uninstall #{NAME}}
|
45
|
+
end
|
46
|
+
|
47
|
+
##############################################################################
|
48
|
+
# Documentation
|
49
|
+
##############################################################################
|
50
|
+
task :doc => [:rdoc]
|
51
|
+
namespace :doc do
|
52
|
+
|
53
|
+
Rake::RDocTask.new do |rdoc|
|
54
|
+
files = ["README", "LICENSE", "CHANGELOG", "lib/**/*.rb"]
|
55
|
+
rdoc.rdoc_files.add(files)
|
56
|
+
rdoc.main = "README"
|
57
|
+
rdoc.title = "Uppercut Docs"
|
58
|
+
rdoc.rdoc_dir = "doc/rdoc"
|
59
|
+
rdoc.options << "--line-numbers" << "--inline-source"
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
##############################################################################
|
66
|
+
# Specs
|
67
|
+
##############################################################################
|
68
|
+
desc "Run all specs"
|
69
|
+
Spec::Rake::SpecTask.new('specs') do |t|
|
70
|
+
t.spec_files = FileList['specs/*_spec.rb']
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class BasicAgent < Uppercut::Agent
|
2
|
+
command 'date' do |m|
|
3
|
+
m.send `date`
|
4
|
+
end
|
5
|
+
|
6
|
+
command /^cat (.*)/ do |m,rest|
|
7
|
+
m.send File.read(rest)
|
8
|
+
end
|
9
|
+
|
10
|
+
command 'report' do |m|
|
11
|
+
m.send 'Hostname: ' + `hostname`
|
12
|
+
m.send 'Running as: ' + ENV['USER']
|
13
|
+
end
|
14
|
+
|
15
|
+
command 'dangerous' do |c|
|
16
|
+
c.send "Are you sure?!"
|
17
|
+
c.wait_for do |reply|
|
18
|
+
c.send %w(yes y).include?(reply.downcase) ? "Okay! Done boss!" : "Cancelled!"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
class Uppercut
|
2
|
+
class Agent < Base
|
3
|
+
class << self
|
4
|
+
# Define a new command for the agent.
|
5
|
+
#
|
6
|
+
# The pattern can be a String or a Regexp. If a String is passed, it
|
7
|
+
# will dispatch this command only on an exact match. A Regexp simply
|
8
|
+
# must match.
|
9
|
+
#
|
10
|
+
# There is always at least one argument sent to the block. The first
|
11
|
+
# is a always an Uppercut::Message object, which can be used to reply
|
12
|
+
# to the sender. The rest of the arguments to the block correspond to
|
13
|
+
# any captures in the pattern Regexp. (Does not apply to String
|
14
|
+
# patterns).
|
15
|
+
def command(pattern,&block)
|
16
|
+
define_method(gensym) do |msg|
|
17
|
+
return :no_match unless captures = matches?(pattern,msg.body)
|
18
|
+
block[Conversation.new(msg.from,self),*captures]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def gensym
|
25
|
+
'__uc' + (self.instance_methods.grep(/^__uc/).size).to_s.rjust(8,'0')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
DEFAULT_OPTIONS = { :connect => true }
|
30
|
+
|
31
|
+
# Create a new instance of an Agent, possibly connecting to the server.
|
32
|
+
#
|
33
|
+
# user should be a String in the form: "user@server/Resource". pw is
|
34
|
+
# simply the password for this account. The final, and optional, argument
|
35
|
+
# is a boolean which controls whether or not it will attempt to connect to
|
36
|
+
# the server immediately. Defaults to true.
|
37
|
+
def initialize(user,pw,options={})
|
38
|
+
options = DEFAULT_OPTIONS.merge(options)
|
39
|
+
|
40
|
+
@user = user
|
41
|
+
@pw = pw
|
42
|
+
connect if options[:connect]
|
43
|
+
listen if options[:listen]
|
44
|
+
|
45
|
+
@allowed_roster = options[:roster]
|
46
|
+
@redirects = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def inspect #:nodoc:
|
51
|
+
"<Uppercut::Agent #{@user} " +
|
52
|
+
"#{listening? ? 'Listening' : 'Not Listening'}:" +
|
53
|
+
"#{connected? ? 'Connected' : 'Disconnected'}>"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Makes an Agent instance begin listening for incoming messages and
|
57
|
+
# subscription requests.
|
58
|
+
#
|
59
|
+
# Current listen simply eats any errors that occur, in the interest of
|
60
|
+
# keeping the remote agent alive. These should be logged at some point
|
61
|
+
# in the future. Pass debug as true to prevent this behaviour.
|
62
|
+
#
|
63
|
+
# Calling listen fires off a new Thread whose sole purpose is to listen
|
64
|
+
# for new incoming messages and then fire off a new Thread which dispatches
|
65
|
+
# the message to the proper handler.
|
66
|
+
def listen(debug=false)
|
67
|
+
connect unless connected?
|
68
|
+
|
69
|
+
@listen_thread = Thread.new {
|
70
|
+
@client.add_message_callback do |message|
|
71
|
+
next if message.body.nil?
|
72
|
+
next unless allowed_roster_includes?(message.from)
|
73
|
+
|
74
|
+
Thread.new do
|
75
|
+
begin
|
76
|
+
dispatch(message)
|
77
|
+
rescue => e
|
78
|
+
log e
|
79
|
+
raise if debug
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@roster ||= Jabber::Roster::Helper.new(@client)
|
84
|
+
@roster.add_subscription_request_callback do |item,presence|
|
85
|
+
next unless allowed_roster_includes?(presence.from)
|
86
|
+
@roster.accept_subscription(presence.from)
|
87
|
+
end
|
88
|
+
sleep
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
# Stops the Agent from listening to incoming messages.
|
93
|
+
#
|
94
|
+
# Simply kills the thread if it is running.
|
95
|
+
def stop
|
96
|
+
@listen_thread.kill if listening?
|
97
|
+
end
|
98
|
+
|
99
|
+
# True if the Agent is currently listening for incoming messages.
|
100
|
+
def listening?
|
101
|
+
@listen_thread && @listen_thread.alive?
|
102
|
+
end
|
103
|
+
|
104
|
+
def redirect_from(contact,&block)
|
105
|
+
@redirects[contact] ||= []
|
106
|
+
@redirects[contact].push block
|
107
|
+
end
|
108
|
+
|
109
|
+
attr_accessor :allowed_roster
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def dispatch(msg)
|
114
|
+
block = @redirects[msg.from].respond_to?(:shift) && @redirects[msg.from].shift
|
115
|
+
return block[msg.body] if block
|
116
|
+
|
117
|
+
self.methods.grep(/^__uc/).sort.detect { |m| send(m,msg) != :no_match }
|
118
|
+
end
|
119
|
+
|
120
|
+
def __ucDefault(msg)
|
121
|
+
Message.new(msg.from,self).send("I don't know what \"#{msg.body}\" means.")
|
122
|
+
end
|
123
|
+
|
124
|
+
def matches?(pattern,msg)
|
125
|
+
captures = nil
|
126
|
+
case pattern
|
127
|
+
when String
|
128
|
+
captures = [] if pattern == msg
|
129
|
+
when Regexp
|
130
|
+
match_data = pattern.match(msg)
|
131
|
+
captures = match_data.captures if match_data
|
132
|
+
end
|
133
|
+
captures
|
134
|
+
end
|
135
|
+
|
136
|
+
def allowed_roster_includes?(jid)
|
137
|
+
return true unless @allowed_roster
|
138
|
+
|
139
|
+
jid = jid.to_s
|
140
|
+
return true if @allowed_roster.include?(jid)
|
141
|
+
return true if @allowed_roster.include?(jid.sub(/\/[^\/]+$/,''))
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class Uppercut
|
2
|
+
class Base
|
3
|
+
def stanza(msg) #:nodoc:
|
4
|
+
return false unless connected?
|
5
|
+
send! msg
|
6
|
+
end
|
7
|
+
|
8
|
+
# Attempt to connect to the server, if not already connected.
|
9
|
+
#
|
10
|
+
# Raises a simple RuntimeError if it fails to connect. This should be
|
11
|
+
# changed eventually to be more useful.
|
12
|
+
def connect
|
13
|
+
return if connected?
|
14
|
+
connect!
|
15
|
+
raise 'Failed to connected' unless connected?
|
16
|
+
present!
|
17
|
+
end
|
18
|
+
|
19
|
+
# Disconnects from the server if it is connected.
|
20
|
+
def disconnect
|
21
|
+
disconnect! if connected?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Disconnects and connects to the server.
|
25
|
+
def reconnect
|
26
|
+
disconnect
|
27
|
+
connect
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :client, :roster
|
31
|
+
|
32
|
+
# True if the Agent is currently connected to the Jabber server.
|
33
|
+
def connected?
|
34
|
+
@client.respond_to?(:is_connected?) && @client.is_connected?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def connect!
|
40
|
+
@connect_lock ||= Mutex.new
|
41
|
+
return if @connect_lock.locked?
|
42
|
+
|
43
|
+
client = Jabber::Client.new(@user)
|
44
|
+
|
45
|
+
@connect_lock.lock
|
46
|
+
|
47
|
+
client.connect
|
48
|
+
client.auth(@pw)
|
49
|
+
@client = client
|
50
|
+
|
51
|
+
@connect_lock.unlock
|
52
|
+
end
|
53
|
+
|
54
|
+
def disconnect!
|
55
|
+
@client.close if connected?
|
56
|
+
@client = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def present!
|
60
|
+
send! Jabber::Presence.new(nil,"Available")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Taken directly from xmpp4r-simple (thanks Blaine!)
|
64
|
+
def send!(msg)
|
65
|
+
attempts = 0
|
66
|
+
begin
|
67
|
+
attempts += 1
|
68
|
+
@client.send(msg)
|
69
|
+
rescue Errno::EPIPE, IOError => e
|
70
|
+
sleep 1
|
71
|
+
disconnect!
|
72
|
+
connect!
|
73
|
+
retry unless attempts > 3
|
74
|
+
raise e
|
75
|
+
rescue Errno::ECONNRESET => e
|
76
|
+
sleep (attempts^2) * 60 + 60
|
77
|
+
disconnect!
|
78
|
+
connect!
|
79
|
+
retry unless attempts > 3
|
80
|
+
raise e
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def log(error)
|
85
|
+
# todo
|
86
|
+
p error
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Uppercut
|
2
|
+
class Conversation < Message
|
3
|
+
attr_reader :to
|
4
|
+
|
5
|
+
def initialize(to,base) #:nodoc:
|
6
|
+
@to = to
|
7
|
+
super base
|
8
|
+
end
|
9
|
+
|
10
|
+
# Wait for another message from this contact.
|
11
|
+
#
|
12
|
+
# Expects a block which should receive one parameter, which will be a
|
13
|
+
# String.
|
14
|
+
#
|
15
|
+
# One common use of _wait_for_ is for confirmation of a sensitive action.
|
16
|
+
#
|
17
|
+
# command('foo') do |c|
|
18
|
+
# c.send 'Are you sure?'
|
19
|
+
# c.wait_for do |reply|
|
20
|
+
# do_it if reply.downcase == 'yes'
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
def wait_for(&block)
|
24
|
+
@base.redirect_from(@to,&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Uppercut
|
2
|
+
class Message
|
3
|
+
attr_accessor :to, :message
|
4
|
+
|
5
|
+
def initialize(base) #:nodoc:
|
6
|
+
@base = base
|
7
|
+
end
|
8
|
+
|
9
|
+
# Send a blob of text.
|
10
|
+
def send(body=nil)
|
11
|
+
msg = Jabber::Message.new(@to)
|
12
|
+
msg.type = :chat
|
13
|
+
msg.body = body || @message
|
14
|
+
@base.stanza(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Uppercut
|
2
|
+
class Notifier < Base
|
3
|
+
class << self
|
4
|
+
@@notifiers = {}
|
5
|
+
|
6
|
+
def notifier(name,&block)
|
7
|
+
@@notifiers[name] = block
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def notify(name,data=nil)
|
12
|
+
return false unless connected?
|
13
|
+
@@notifiers[name].call(Message.new(self),data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(user,pw,options={})
|
17
|
+
options = DEFAULT_OPTIONS.merge(options)
|
18
|
+
|
19
|
+
@user = user
|
20
|
+
@pw = pw
|
21
|
+
connect if options[:connect]
|
22
|
+
end
|
23
|
+
|
24
|
+
DEFAULT_OPTIONS = { :connect => true }
|
25
|
+
|
26
|
+
def inspect #:nodoc:
|
27
|
+
"<Uppercut::Notifier #{@user} " +
|
28
|
+
"#{connected? ? 'Connected' : 'Disconnected'}>"
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/uppercut.rb
ADDED
data/specs/agent_spec.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe Uppercut::Agent do
|
4
|
+
before :each do
|
5
|
+
@agent = TestAgent.new('test@foo.com', 'pw', :connect => false)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe :new do
|
9
|
+
it "connects by default" do
|
10
|
+
agent = Uppercut::Agent.new('test@foo','pw')
|
11
|
+
agent.should be_connected
|
12
|
+
end
|
13
|
+
|
14
|
+
it "does not connect by default with :connect = false" do
|
15
|
+
agent = Uppercut::Agent.new('test@foo','pw', :connect => false)
|
16
|
+
agent.should_not be_connected
|
17
|
+
end
|
18
|
+
|
19
|
+
it "starts to listen with :listen = true" do
|
20
|
+
agent = Uppercut::Agent.new('test@foo','pw', :listen => true)
|
21
|
+
agent.should be_listening
|
22
|
+
end
|
23
|
+
|
24
|
+
it "initializes @redirects with a blank hash" do
|
25
|
+
agent = Uppercut::Agent.new('test@foo','pw', :connect => false)
|
26
|
+
agent.instance_eval { @redirects }.should == {}
|
27
|
+
end
|
28
|
+
|
29
|
+
it "populates @pw and @user" do
|
30
|
+
agent = Uppercut::Agent.new('test@foo','pw')
|
31
|
+
agent.instance_eval { @pw }.should == 'pw'
|
32
|
+
agent.instance_eval { @user }.should == 'test@foo'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "populates @allowed_roster with :roster option" do
|
36
|
+
jids = %w(bob@foo fred@foo)
|
37
|
+
agent = Uppercut::Agent.new('test@foo','pw', :roster => jids)
|
38
|
+
agent.instance_eval { @allowed_roster }.should == jids
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe :connect do
|
43
|
+
it "does not try to connect if already connected" do
|
44
|
+
@agent.connect
|
45
|
+
old_client = @agent.client
|
46
|
+
|
47
|
+
@agent.connect
|
48
|
+
(@agent.client == old_client).should == true
|
49
|
+
end
|
50
|
+
|
51
|
+
it "connects if disconnected" do
|
52
|
+
@agent.should_not be_connected
|
53
|
+
|
54
|
+
old_client = @agent.client
|
55
|
+
|
56
|
+
@agent.connect
|
57
|
+
(@agent.client == old_client).should_not == true
|
58
|
+
end
|
59
|
+
|
60
|
+
it "sends a Presence notification" do
|
61
|
+
@agent.connect
|
62
|
+
@agent.client.sent.first.class.should == Jabber::Presence
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe :disconnect do
|
67
|
+
it "does not try to disconnect if not connected" do
|
68
|
+
@agent.client.should be_nil
|
69
|
+
@agent.instance_eval { @client = :foo }
|
70
|
+
|
71
|
+
@agent.disconnect
|
72
|
+
@agent.client.should == :foo
|
73
|
+
end
|
74
|
+
|
75
|
+
it "sets @client to nil" do
|
76
|
+
@agent.connect
|
77
|
+
@agent.client.should_not be_nil
|
78
|
+
|
79
|
+
@agent.disconnect
|
80
|
+
@agent.client.should be_nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe :reconnect do
|
85
|
+
it "calls disconnect then connect" do
|
86
|
+
@agent.should_receive(:disconnect).once.ordered
|
87
|
+
@agent.should_receive(:connect).once.ordered
|
88
|
+
|
89
|
+
@agent.reconnect
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe :connected? do
|
94
|
+
it "returns true if client#is_connected? is true" do
|
95
|
+
@agent.connect
|
96
|
+
@agent.client.instance_eval { @connected = true }
|
97
|
+
@agent.should be_connected
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe :listen do
|
102
|
+
it "connects if not connected" do
|
103
|
+
@agent.listen
|
104
|
+
@agent.should be_connected
|
105
|
+
end
|
106
|
+
|
107
|
+
it "spins off a new thread in @listen_thread" do
|
108
|
+
@agent.listen
|
109
|
+
@agent.instance_eval { @listen_thread.class }.should == Thread
|
110
|
+
end
|
111
|
+
|
112
|
+
it "creates a receive message callback" do
|
113
|
+
@agent.listen
|
114
|
+
@agent.client.on_message.class.should == Proc
|
115
|
+
end
|
116
|
+
|
117
|
+
it "creates a subscription request callback" do
|
118
|
+
@agent.listen
|
119
|
+
@agent.roster.on_subscription_request.class.should == Proc
|
120
|
+
end
|
121
|
+
|
122
|
+
it "calls dispatch when receving a message" do
|
123
|
+
@agent.listen
|
124
|
+
@agent.should_receive(:dispatch)
|
125
|
+
@agent.client.receive_message("foo@bar.com","test")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe :stop do
|
130
|
+
it "kills the @listen_thread" do
|
131
|
+
@agent.listen
|
132
|
+
@agent.instance_eval { @listen_thread.alive? }.should == true
|
133
|
+
|
134
|
+
@agent.stop
|
135
|
+
@agent.instance_eval { @listen_thread.alive? }.should_not == true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe :listening? do
|
140
|
+
it "returns true if @listen_thread is alive" do
|
141
|
+
@agent.listen
|
142
|
+
@agent.instance_eval { @listen_thread.alive? }.should == true
|
143
|
+
@agent.should be_listening
|
144
|
+
end
|
145
|
+
|
146
|
+
it "returns false if @listen_thread is not alive" do
|
147
|
+
@agent.listen
|
148
|
+
@agent.stop
|
149
|
+
@agent.should_not be_listening
|
150
|
+
end
|
151
|
+
|
152
|
+
it "returns false if @listen_thread has not been set" do
|
153
|
+
@agent.should_not be_listening
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe :dispatch do
|
158
|
+
it "calls the first matching command" do
|
159
|
+
msg = Jabber::Message.new(nil)
|
160
|
+
msg.body = 'hi'
|
161
|
+
|
162
|
+
@agent.send(:dispatch, msg)
|
163
|
+
@agent.instance_eval { @called_hi_regex }.should_not == true
|
164
|
+
@agent.instance_eval { @called_hi }.should == true
|
165
|
+
end
|
166
|
+
|
167
|
+
it "matches by regular expression" do
|
168
|
+
msg = Jabber::Message.new(nil)
|
169
|
+
msg.body = 'high'
|
170
|
+
|
171
|
+
@agent.send(:dispatch, msg)
|
172
|
+
@agent.instance_eval { @called_hi }.should_not == true
|
173
|
+
@agent.instance_eval { @called_hi_regex }.should == true
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
|
4
|
+
describe Uppercut::Conversation do
|
5
|
+
before(:each) do
|
6
|
+
@conv = Uppercut::Conversation.new('test@foo.com', nil)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe :contact do
|
10
|
+
it "should have a contact method" do
|
11
|
+
@conv.should.respond_to?(:contact)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
Object.send(:remove_const, :Jabber)
|
2
|
+
class Jabber
|
3
|
+
class Client
|
4
|
+
def initialize(user)
|
5
|
+
@user = user
|
6
|
+
end
|
7
|
+
|
8
|
+
def connect
|
9
|
+
@connected = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def auth(pw)
|
13
|
+
@pw = pw
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_connected?
|
17
|
+
@connected
|
18
|
+
end
|
19
|
+
|
20
|
+
def close
|
21
|
+
@connected = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :sent
|
25
|
+
def send(msg)
|
26
|
+
@sent ||= []
|
27
|
+
@sent << msg
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :on_message
|
31
|
+
def add_message_callback(&block)
|
32
|
+
@on_message = block
|
33
|
+
end
|
34
|
+
|
35
|
+
# TESTING HELPER METHODS
|
36
|
+
|
37
|
+
def receive_message(from,body,type=:chat)
|
38
|
+
msg = Message.new(nil)
|
39
|
+
msg.type = type
|
40
|
+
msg.body = body
|
41
|
+
msg.from = from
|
42
|
+
@on_message[msg]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Presence
|
47
|
+
def initialize(a,b)
|
48
|
+
end
|
49
|
+
|
50
|
+
def from
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Message
|
55
|
+
attr_accessor :type, :body, :from
|
56
|
+
def initialize(to)
|
57
|
+
@to = to
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Roster
|
62
|
+
class Helper
|
63
|
+
def initialize(client)
|
64
|
+
@client = client
|
65
|
+
end
|
66
|
+
|
67
|
+
def accept_subscription(a)
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :on_subscription_request
|
71
|
+
def add_subscription_request_callback(&block)
|
72
|
+
@on_subscription_request = block
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe Uppercut::Notifier do
|
4
|
+
before :each do
|
5
|
+
@notifier = TestNotifier.new('test@foo.com', 'pw', :connect => false)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe :new do
|
9
|
+
it "connects by default" do
|
10
|
+
notifier = Uppercut::Notifier.new('test@foo','pw')
|
11
|
+
notifier.should be_connected
|
12
|
+
end
|
13
|
+
|
14
|
+
it "does not connect by default with :connect = false" do
|
15
|
+
notifier = Uppercut::Notifier.new('test@foo','pw', :connect => false)
|
16
|
+
notifier.should_not be_connected
|
17
|
+
end
|
18
|
+
|
19
|
+
it "populates @pw and @user" do
|
20
|
+
notifier = Uppercut::Notifier.new('test@foo','pw')
|
21
|
+
notifier.instance_eval { @pw }.should == 'pw'
|
22
|
+
notifier.instance_eval { @user }.should == 'test@foo'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe :connect do
|
27
|
+
it "does not try to connect if already connected" do
|
28
|
+
@notifier.connect
|
29
|
+
old_client = @notifier.client
|
30
|
+
|
31
|
+
@notifier.connect
|
32
|
+
(@notifier.client == old_client).should == true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "connects if disconnected" do
|
36
|
+
@notifier.should_not be_connected
|
37
|
+
|
38
|
+
old_client = @notifier.client
|
39
|
+
|
40
|
+
@notifier.connect
|
41
|
+
(@notifier.client == old_client).should_not == true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "sends a Presence notification" do
|
45
|
+
@notifier.connect
|
46
|
+
@notifier.client.sent.first.class.should == Jabber::Presence
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe :disconnect do
|
51
|
+
it "does not try to disconnect if not connected" do
|
52
|
+
@notifier.client.should be_nil
|
53
|
+
@notifier.instance_eval { @client = :foo }
|
54
|
+
|
55
|
+
@notifier.disconnect
|
56
|
+
@notifier.client.should == :foo
|
57
|
+
end
|
58
|
+
|
59
|
+
it "sets @client to nil" do
|
60
|
+
@notifier.connect
|
61
|
+
@notifier.client.should_not be_nil
|
62
|
+
|
63
|
+
@notifier.disconnect
|
64
|
+
@notifier.client.should be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe :reconnect do
|
69
|
+
it "calls disconnect then connect" do
|
70
|
+
@notifier.should_receive(:disconnect).once.ordered
|
71
|
+
@notifier.should_receive(:connect).once.ordered
|
72
|
+
|
73
|
+
@notifier.reconnect
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe :connected? do
|
78
|
+
it "returns true if client#is_connected? is true" do
|
79
|
+
@notifier.connect
|
80
|
+
@notifier.client.instance_eval { @connected = true }
|
81
|
+
@notifier.should be_connected
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
$: << File.dirname(__FILE__)
|
6
|
+
$: << File.join(File.dirname(__FILE__),'../lib')
|
7
|
+
|
8
|
+
# Loads uppercut and jabber
|
9
|
+
require 'uppercut'
|
10
|
+
|
11
|
+
# Unloads jabber, replacing it with a stub
|
12
|
+
require 'jabber_stub'
|
13
|
+
|
14
|
+
class TestAgent < Uppercut::Agent
|
15
|
+
command 'hi' do |c|
|
16
|
+
c.instance_eval { @base.instance_eval { @called_hi = true } }
|
17
|
+
c.send 'called hi'
|
18
|
+
end
|
19
|
+
|
20
|
+
command /^hi/ do |c|
|
21
|
+
c.instance_eval { @base.instance_eval { @called_hi_regex = true } }
|
22
|
+
c.send 'called high regex'
|
23
|
+
end
|
24
|
+
|
25
|
+
command /(good)?bye/ do |c,good|
|
26
|
+
@called_goodbye = true
|
27
|
+
c.send good ? "Good bye to you as well!" : "Rot!"
|
28
|
+
end
|
29
|
+
|
30
|
+
command 'wait' do |c|
|
31
|
+
@called_wait = true
|
32
|
+
c.send 'Waiting...'
|
33
|
+
c.wait_for do |reply|
|
34
|
+
@called_wait_block = true
|
35
|
+
c.send 'Hooray!'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class TestNotifier < Uppercut::Notifier
|
41
|
+
notifier :foo do |m,data|
|
42
|
+
m.to = 'foo@bar.com'
|
43
|
+
m.send 'Foo happened!'
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tyler-uppercut
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tyler McMullen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-10-09 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: xmpp4r
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "0"
|
23
|
+
version:
|
24
|
+
description: Uppercut. DSL for putting Jabber to work for you.
|
25
|
+
email: tbmcmullen@gmail.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- LICENSE
|
34
|
+
- README.textile
|
35
|
+
- Rakefile
|
36
|
+
- lib/uppercut.rb
|
37
|
+
- lib/uppercut/agent.rb
|
38
|
+
- lib/uppercut/base.rb
|
39
|
+
- lib/uppercut/conversation.rb
|
40
|
+
- lib/uppercut/message.rb
|
41
|
+
- lib/uppercut/notifier.rb
|
42
|
+
- specs/agent_spec.rb
|
43
|
+
- specs/conversation_spec.rb
|
44
|
+
- specs/jabber_stub.rb
|
45
|
+
- specs/notifier_spec.rb
|
46
|
+
- specs/spec_helper.rb
|
47
|
+
- examples/basic_agent.rb
|
48
|
+
has_rdoc: false
|
49
|
+
homepage: http://codehallow.com
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.2.0
|
71
|
+
signing_key:
|
72
|
+
specification_version: 2
|
73
|
+
summary: Uppercut. DSL for putting Jabber to work for you.
|
74
|
+
test_files: []
|
75
|
+
|