julien51-babylon 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +52 -0
- data/bin/babylon +3 -1
- data/lib/babylon.rb +82 -0
- data/lib/babylon/base/controller.rb +76 -0
- data/lib/babylon/base/view.rb +34 -0
- data/lib/babylon/client_connection.rb +188 -0
- data/lib/babylon/component_connection.rb +71 -0
- data/lib/babylon/router.rb +117 -0
- data/lib/babylon/router/dsl.rb +61 -0
- data/lib/babylon/runner.rb +57 -0
- data/lib/babylon/xmpp_connection.rb +223 -0
- data/lib/babylon/xpath_helper.rb +13 -0
- data/templates/babylon/app/controllers/README.rdoc +13 -0
- data/templates/babylon/app/models/README.rdoc +1 -0
- data/templates/babylon/app/views/README.rdoc +12 -0
- data/templates/babylon/config/boot.rb +9 -0
- data/templates/babylon/config/config.yaml +26 -0
- data/templates/babylon/config/dependencies.rb +2 -0
- data/templates/babylon/config/initializers/README.rdoc +1 -0
- data/templates/babylon/config/routes.rb +24 -0
- metadata +22 -3
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "babylon"
|
8
|
+
gem.summary = %Q{Babylon is a framework to create EventMachine based XMPP External Components in Ruby.}
|
9
|
+
gem.email = "julien.genestoux@gmail.com"
|
10
|
+
gem.homepage = "http://github.com/julien51/babylon"
|
11
|
+
gem.authors = ["julien Genestoux"]
|
12
|
+
gem.requirements = ["eventmachine", "yaml", "fileutils", "log4r", "nokogiri"]
|
13
|
+
gem.executables = "babylon"
|
14
|
+
gem.files = ["bin/babylon", "lib/babylon.rb", "lib/babylon/base/controller.rb", "lib/babylon/base/view.rb", "lib/babylon/client_connection.rb", "lib/babylon/component_connection.rb", "lib/babylon/router/dsl.rb", "lib/babylon/router.rb", "lib/babylon/runner.rb", "lib/babylon/xmpp_connection.rb", "lib/babylon/xpath_helper.rb", "LICENSE", "Rakefile", "README.rdoc", "templates/babylon/app/controllers/README.rdoc", "templates/babylon/app/models/README.rdoc", "templates/babylon/app/views/README.rdoc", "templates/babylon/config/boot.rb", "templates/babylon/config/config.yaml", "templates/babylon/config/dependencies.rb", "templates/babylon/config/routes.rb", "templates/babylon/config/initializers/README.rdoc"]
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/rdoctask'
|
22
|
+
Rake::RDocTask.new do |rdoc|
|
23
|
+
rdoc.rdoc_dir = 'rdoc'
|
24
|
+
rdoc.title = 'babylon'
|
25
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
26
|
+
rdoc.rdoc_files.include('README*')
|
27
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'rake/testtask'
|
31
|
+
Rake::TestTask.new(:test) do |test|
|
32
|
+
test.libs << 'lib' << 'test'
|
33
|
+
test.pattern = 'test/**/*_test.rb'
|
34
|
+
test.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
require 'rcov/rcovtask'
|
39
|
+
Rcov::RcovTask.new do |test|
|
40
|
+
test.libs << 'test'
|
41
|
+
test.pattern = 'test/**/*_test.rb'
|
42
|
+
test.verbose = true
|
43
|
+
end
|
44
|
+
rescue LoadError
|
45
|
+
task :rcov do
|
46
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
task :install => :build
|
51
|
+
|
52
|
+
task :default => :test
|
data/bin/babylon
CHANGED
@@ -4,9 +4,11 @@
|
|
4
4
|
# This will generate the right hierarchy for a Babylon App
|
5
5
|
# First, let's create the app directoryn based in ARGV[0]
|
6
6
|
|
7
|
+
require 'fileutils'
|
8
|
+
|
7
9
|
if ARGV[0]
|
8
10
|
puts "Creating app '#{ARGV[0]}' in #{Dir.pwd}..."
|
9
11
|
FileUtils.cp_r "#{File.dirname(__FILE__)}/../templates/babylon", "#{Dir.pwd}/#{ARGV[0]}"
|
10
12
|
else
|
11
|
-
puts "
|
13
|
+
puts "Usage: #{$0} <app_name>"
|
12
14
|
end
|
data/lib/babylon.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'log4r'
|
6
|
+
require 'nokogiri'
|
7
|
+
require 'yaml'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
require 'babylon/xmpp_connection'
|
11
|
+
require 'babylon/component_connection'
|
12
|
+
require 'babylon/client_connection'
|
13
|
+
require 'babylon/router'
|
14
|
+
require 'babylon/runner'
|
15
|
+
require "babylon/xpath_helper"
|
16
|
+
require 'babylon/base/controller'
|
17
|
+
require 'babylon/base/view'
|
18
|
+
|
19
|
+
# Babylon is a XMPP Component Framework based on EventMachine. It uses the Nokogiri GEM, which is a Ruby wrapper for Libxml2.
|
20
|
+
# It implements the MVC paradigm.
|
21
|
+
# You can create your own application by running :
|
22
|
+
# $> babylon app_name
|
23
|
+
# This will generate some folders and files for your application. Please see README for further instructions
|
24
|
+
|
25
|
+
module Babylon
|
26
|
+
|
27
|
+
def self.environment=(_env)
|
28
|
+
@@env = _env
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.environment
|
32
|
+
unless self.class_variable_defined?("@@env")
|
33
|
+
@@env = "development"
|
34
|
+
end
|
35
|
+
@@env
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Caches the view files to improve performance.
|
40
|
+
def self.cache_views
|
41
|
+
@@cached_views= {}
|
42
|
+
Dir.glob('app/views/*/*').each do |f|
|
43
|
+
@@cached_views[f] = File.read(f)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.cached_views
|
48
|
+
unless self.class_variable_defined?("@@cached_views")
|
49
|
+
@@cached_views= {}
|
50
|
+
end
|
51
|
+
@@cached_views
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Returns a shared logger for this component.
|
56
|
+
def self.logger
|
57
|
+
unless self.class_variable_defined?("@@logger")
|
58
|
+
@@logger = Log4r::Logger.new("BABYLON")
|
59
|
+
@@logger.add(Log4r::Outputter.stderr)
|
60
|
+
end
|
61
|
+
@@logger
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Set the configuration for this component.
|
66
|
+
def self.config=(conf)
|
67
|
+
@@config = conf
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Return the configuration for this component.
|
72
|
+
def self.config
|
73
|
+
@@config
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Authentication Error (wrong password/jid combination). Used for Clients and Components
|
78
|
+
class AuthenticationError < Exception
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Babylon
|
2
|
+
module Base
|
3
|
+
|
4
|
+
##
|
5
|
+
# Your application's controller should be descendant of this class.
|
6
|
+
class Controller
|
7
|
+
|
8
|
+
attr_accessor :stanza, :rendered, :action_name # Stanza received by the controller (Nokogiri::XML::Node)
|
9
|
+
|
10
|
+
##
|
11
|
+
# Creates a new controller (you should not override this class) and assigns the stanza as well as any other value of the hash to instances named after the keys of the hash.
|
12
|
+
def initialize(params = {})
|
13
|
+
params.each do |key, value|
|
14
|
+
instance_variable_set("@#{key}", value)
|
15
|
+
end
|
16
|
+
@rendered = false
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Performs the action and calls back the optional block argument : you should not override this function
|
21
|
+
def perform(action, &block)
|
22
|
+
@action_name = action
|
23
|
+
@block = block
|
24
|
+
begin
|
25
|
+
self.send(@action_name)
|
26
|
+
rescue
|
27
|
+
Babylon.logger.error("#{$!}:\n#{$!.backtrace.join("\n")}")
|
28
|
+
end
|
29
|
+
self.render
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Called by default after each action to "build" a XMPP stanza. By default, it will use the /controller_name/action.xml.builder
|
34
|
+
def render(options = nil)
|
35
|
+
return if @rendered # Avoid double rendering
|
36
|
+
|
37
|
+
if options.nil? # default rendering
|
38
|
+
return render(:file => default_template_name)
|
39
|
+
elsif options[:file]
|
40
|
+
render_for_file(view_path(options[:file]))
|
41
|
+
elsif action_name = options[:action]
|
42
|
+
return render(:file => default_template_name(action_name.to_s))
|
43
|
+
end
|
44
|
+
|
45
|
+
# And finally, we set up rendered to be true
|
46
|
+
@rendered = true
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
# Used to transfer the assigned variables from the controller to the views
|
52
|
+
def hashed_variables
|
53
|
+
vars = Hash.new
|
54
|
+
instance_variables.each do |var|
|
55
|
+
vars[var[1..-1]] = instance_variable_get(var)
|
56
|
+
end
|
57
|
+
return vars
|
58
|
+
end
|
59
|
+
|
60
|
+
def view_path(file_name)
|
61
|
+
File.join("app/views", "#{self.class.name.gsub("Controller","").downcase}", file_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Default template name used to build stanzas
|
65
|
+
def default_template_name(action_name = nil)
|
66
|
+
return "#{action_name || @action_name}.xml.builder"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Creates the view and "evaluates" it to build the XML for the stanza
|
70
|
+
def render_for_file(file)
|
71
|
+
Babylon.logger.info("RENDERING : #{file}")
|
72
|
+
@block.call(Babylon::Base::View.new(file, hashed_variables).evaluate) if @block
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Babylon
|
2
|
+
module Base
|
3
|
+
|
4
|
+
##
|
5
|
+
# Your application's views (stanzas) should be descendant of this class.
|
6
|
+
class View
|
7
|
+
attr_reader :output, :view_template
|
8
|
+
|
9
|
+
##
|
10
|
+
# Instantiate a new view with the various varibales passed in assigns and the path of the template to render.
|
11
|
+
def initialize(path, assigns)
|
12
|
+
@output = ""
|
13
|
+
@view_template = path
|
14
|
+
assigns.each do |key, value|
|
15
|
+
instance_variable_set("@#{key}", value)
|
16
|
+
self.class.send(:define_method, key) do # Defining accessors
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# "Loads" the view file, and uses the Nokogiri Builder to build the XML stanzas that will be sent.
|
24
|
+
def evaluate
|
25
|
+
str = (Babylon.cached_views && Babylon.cached_views[@view_template]) ? Babylon.cached_views[@view_template] : File.read(@view_template)
|
26
|
+
xml = Nokogiri::XML::Builder.new do
|
27
|
+
instance_eval(str)
|
28
|
+
end
|
29
|
+
return xml.doc.children #we return the doc's children (to avoid the instruct)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Babylon
|
2
|
+
|
3
|
+
##
|
4
|
+
# ClientConnection is in charge of the XMPP connection for a Regular XMPP Client.
|
5
|
+
# So far, SASL Plain authenticationonly is supported
|
6
|
+
# Upon stanza reception, and depending on the status (connected... etc), this component will handle or forward the stanzas.
|
7
|
+
class ClientConnection < XmppConnection
|
8
|
+
require 'digest/sha1'
|
9
|
+
require 'base64'
|
10
|
+
require 'resolv'
|
11
|
+
|
12
|
+
|
13
|
+
attr_reader :binding_iq_id, :session_iq_id
|
14
|
+
|
15
|
+
##
|
16
|
+
# Creates a new ClientConnection and waits for data in the stream
|
17
|
+
def initialize(params)
|
18
|
+
super(params)
|
19
|
+
@state = :wait_for_stream
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Connects the ClientConnection based on SRV records for the jid's domain, if no host or port has been specified.
|
24
|
+
# In any case, we give priority to the specified host and port.
|
25
|
+
def self.connect(params, &block)
|
26
|
+
return super(params, &block) if params["host"] && params["port"]
|
27
|
+
|
28
|
+
begin
|
29
|
+
begin
|
30
|
+
srv = []
|
31
|
+
Resolv::DNS.open { |dns|
|
32
|
+
# If ruby version is too old and SRV is unknown, this will raise a NameError
|
33
|
+
# which is caught below
|
34
|
+
host_from_jid = params["jid"].split("/").first.split("@").last
|
35
|
+
Babylon.logger.debug("RESOLVING: _xmpp-client._tcp.#{host_from_jid} (SRV)")
|
36
|
+
srv = dns.getresources("_xmpp-client._tcp.#{host_from_jid}", Resolv::DNS::Resource::IN::SRV)
|
37
|
+
}
|
38
|
+
# Sort SRV records: lowest priority first, highest weight first
|
39
|
+
srv.sort! { |a,b| (a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight) }
|
40
|
+
# And now, for each record, let's try to connect.
|
41
|
+
srv.each { |record|
|
42
|
+
begin
|
43
|
+
params["host"] = record.target.to_s
|
44
|
+
params["port"] = Integer(record.port)
|
45
|
+
super(params, &block)
|
46
|
+
# Success
|
47
|
+
break
|
48
|
+
rescue SocketError, Errno::ECONNREFUSED
|
49
|
+
# Try next SRV record
|
50
|
+
end
|
51
|
+
}
|
52
|
+
rescue NameError
|
53
|
+
Babylon.logger.debug "Resolv::DNS does not support SRV records. Please upgrade to ruby-1.8.3 or later! \n #{$!} : #{$!.inspect}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
##
|
60
|
+
# Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
|
61
|
+
# to establish the XMPP connection itself.
|
62
|
+
# We use a "tweak" here to send only the starting tag of stream:stream
|
63
|
+
def connection_completed
|
64
|
+
super
|
65
|
+
builder = Nokogiri::XML::Builder.new do
|
66
|
+
self.send('stream:stream', {'xmlns' => @context.stream_namespace(), 'xmlns:stream' => 'http://etherx.jabber.org/streams', 'to' => @context.jid.split("/").first.split("@").last, 'version' => '1.0'}) do
|
67
|
+
paste_content_here # The stream:stream element should be cut here ;)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
@outstream = builder.doc
|
71
|
+
start_stream, stop_stream = builder.to_xml.split('<paste_content_here/>')
|
72
|
+
send(start_stream)
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Called upon stanza reception
|
77
|
+
# Marked as connected when the client has been SASLed, authenticated, biund to a resource and when the session has been created
|
78
|
+
def receive_stanza(stanza)
|
79
|
+
case @state
|
80
|
+
when :connected
|
81
|
+
super # Can be dispatched
|
82
|
+
|
83
|
+
when :wait_for_stream
|
84
|
+
if stanza.name == "stream:stream" && stanza.attributes['id']
|
85
|
+
@state = :wait_for_auth_mechanisms unless @success
|
86
|
+
@state = :wait_for_bind if @success
|
87
|
+
end
|
88
|
+
|
89
|
+
when :wait_for_auth_mechanisms
|
90
|
+
if stanza.name == "stream:features"
|
91
|
+
if stanza.at("starttls") # we shall start tls
|
92
|
+
starttls = Nokogiri::XML::Node.new("starttls", @outstream)
|
93
|
+
starttls["xmlns"] = stanza.at("starttls").namespaces.first.last
|
94
|
+
send(starttls)
|
95
|
+
@state = :wait_for_proceed
|
96
|
+
elsif stanza.at("mechanisms") # tls is ok
|
97
|
+
if stanza.at("mechanisms/[contains(mechanism,'PLAIN')]")
|
98
|
+
# auth_text = "#{jid.strip}\x00#{jid.node}\x00#{password}"
|
99
|
+
auth = Nokogiri::XML::Node.new("auth", @outstream)
|
100
|
+
auth['mechanism'] = "PLAIN"
|
101
|
+
auth['xmlns'] = stanza.at("mechanisms").namespaces.first.last
|
102
|
+
auth.content = Base64::encode64([jid, jid.split("@").first, @password].join("\000")).gsub(/\s/, '')
|
103
|
+
send(auth)
|
104
|
+
@state = :wait_for_success
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
when :wait_for_success
|
110
|
+
if stanza.name == "success" # Yay! Success
|
111
|
+
@success = true
|
112
|
+
@state = :wait_for_stream
|
113
|
+
@parser.reset
|
114
|
+
send @outstream.root.to_xml.split('<paste_content_here/>').first
|
115
|
+
elsif stanza.name == "failure"
|
116
|
+
if stanza.at("bad-auth") || stanza.at("not-authorized")
|
117
|
+
raise AuthenticationError
|
118
|
+
else
|
119
|
+
end
|
120
|
+
else
|
121
|
+
# Hum Failure...
|
122
|
+
end
|
123
|
+
|
124
|
+
when :wait_for_bind
|
125
|
+
if stanza.name == "stream:features"
|
126
|
+
if stanza.at("bind")
|
127
|
+
# Let's build the binding_iq
|
128
|
+
@binding_iq_id = Integer(rand(10000))
|
129
|
+
builder = Nokogiri::XML::Builder.new do
|
130
|
+
iq(:type => "set", :id => @context.binding_iq_id) do
|
131
|
+
bind(:xmlns => "urn:ietf:params:xml:ns:xmpp-bind") do
|
132
|
+
if @context.jid.split("/").size == 2
|
133
|
+
resource(@context.jid.split("/").last)
|
134
|
+
else
|
135
|
+
resource("babylon_client_#{@context.binding_iq_id}")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
iq = @outstream.add_child(builder.doc.root)
|
141
|
+
send(iq)
|
142
|
+
@state = :wait_for_confirmed_binding
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
when :wait_for_confirmed_binding
|
147
|
+
if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == @binding_iq_id
|
148
|
+
if stanza.at("jid")
|
149
|
+
jid= stanza.at("jid").text
|
150
|
+
end
|
151
|
+
end
|
152
|
+
# And now, we must initiate the session
|
153
|
+
@session_iq_id = Integer(rand(10000))
|
154
|
+
builder = Nokogiri::XML::Builder.new do
|
155
|
+
iq(:type => "set", :id => @context.session_iq_id) do
|
156
|
+
session(:xmlns => "urn:ietf:params:xml:ns:xmpp-session")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
iq = @outstream.add_child(builder.doc.root)
|
160
|
+
send(iq)
|
161
|
+
@state = :wait_for_confirmed_session
|
162
|
+
|
163
|
+
when :wait_for_confirmed_session
|
164
|
+
if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == @session_iq_id && stanza.at("session")
|
165
|
+
# And now, send a presence!
|
166
|
+
presence = Nokogiri::XML::Node.new("presence", @outstream)
|
167
|
+
send(presence)
|
168
|
+
@connection_callback.call(self) if @connection_callback
|
169
|
+
@state = :connected
|
170
|
+
end
|
171
|
+
|
172
|
+
when :wait_for_proceed
|
173
|
+
start_tls() # starting TLS
|
174
|
+
@state = :wait_for_stream
|
175
|
+
@parser.reset
|
176
|
+
send @outstream.root.to_xml.split('<paste_content_here/>').first
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Namespace of the client
|
183
|
+
def stream_namespace
|
184
|
+
"jabber:client"
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Babylon
|
2
|
+
##
|
3
|
+
# ComponentConnection is in charge of the XMPP connection itself.
|
4
|
+
# Upon stanza reception, and depending on the status (connected... etc), this component will handle or forward the stanzas.
|
5
|
+
class ComponentConnection < XmppConnection
|
6
|
+
require 'digest/sha1'
|
7
|
+
|
8
|
+
##
|
9
|
+
# Creates a new ComponentConnection and waits for data in the stream
|
10
|
+
def initialize(params)
|
11
|
+
super(params)
|
12
|
+
@state = :wait_for_stream
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
|
17
|
+
# to establish the XMPP connection itself.
|
18
|
+
# We use a "tweak" here to send only the starting tag of stream:stream
|
19
|
+
def connection_completed
|
20
|
+
super
|
21
|
+
builder = Nokogiri::XML::Builder.new do
|
22
|
+
self.send('stream:stream', {'xmlns' => "jabber:component:accept", 'xmlns:stream' => 'http://etherx.jabber.org/streams', 'to' => @context.jid}) do
|
23
|
+
paste_content_here # The stream:stream element should be cut here ;)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
@start_stream, @stop_stream = builder.to_xml.split('<paste_content_here/>')
|
27
|
+
send(@start_stream)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# XMPP Component handshake as defined in XEP-0114:
|
32
|
+
# http://xmpp.org/extensions/xep-0114.html
|
33
|
+
def receive_stanza(stanza)
|
34
|
+
case @state
|
35
|
+
when :connected # Most frequent case
|
36
|
+
super # Can be dispatched
|
37
|
+
|
38
|
+
when :wait_for_stream
|
39
|
+
if stanza.name == "stream:stream" && stanza.attributes['id']
|
40
|
+
# This means the XMPP session started!
|
41
|
+
# We must send the handshake now.
|
42
|
+
hash = Digest::SHA1::hexdigest(stanza.attributes['id'].content + @password)
|
43
|
+
handshake = Nokogiri::XML::Node.new("handshake", stanza.document)
|
44
|
+
handshake.content = hash
|
45
|
+
send(handshake)
|
46
|
+
@state = :wait_for_handshake
|
47
|
+
else
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
|
51
|
+
when :wait_for_handshake
|
52
|
+
if stanza.name == "handshake"
|
53
|
+
@connection_callback.call(self) if @connection_callback
|
54
|
+
@state = :connected
|
55
|
+
elsif stanza.name == "stream:error"
|
56
|
+
raise AuthenticationError
|
57
|
+
else
|
58
|
+
raise
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Namespace of the component
|
66
|
+
def stream_namespace
|
67
|
+
'jabber:component:accept'
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require File.dirname(__FILE__)+"/router/dsl"
|
2
|
+
|
3
|
+
module Babylon
|
4
|
+
|
5
|
+
##
|
6
|
+
# The router is in charge of sending the right stanzas to the right controllers based on user defined Routes.
|
7
|
+
module Router
|
8
|
+
|
9
|
+
@@connection = nil
|
10
|
+
|
11
|
+
##
|
12
|
+
# Add several routes to the router
|
13
|
+
# Routes should be of form {name => params}
|
14
|
+
def add_routes(routes)
|
15
|
+
routes.each do |name, params|
|
16
|
+
add_route(Route.new(params))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Connected is called by the XmppConnection to indicate that the XMPP connection has been established
|
22
|
+
def connected(connection)
|
23
|
+
@@connection = connection
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Accessor for @@connection
|
28
|
+
def connection
|
29
|
+
@@connection
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Insert a route and makes sure that the routes are sorted
|
34
|
+
def add_route(route)
|
35
|
+
@routes ||= []
|
36
|
+
@routes << route
|
37
|
+
sort
|
38
|
+
end
|
39
|
+
|
40
|
+
# Look for the first matching route and calls the corresponding action for the corresponding controller.
|
41
|
+
# Sends the response on the XMPP stream/
|
42
|
+
def route(stanza)
|
43
|
+
return false if !@@connection
|
44
|
+
@routes ||= []
|
45
|
+
@routes.each { |route|
|
46
|
+
if route.accepts?(stanza)
|
47
|
+
# Here should happen the magic : call the controller
|
48
|
+
Babylon.logger.info("ROUTING TO #{route.controller}::#{route.action}")
|
49
|
+
controller = route.controller.new({:stanza => stanza})
|
50
|
+
controller.perform(route.action) do |response|
|
51
|
+
# Response should be a Nokogiri::Nodeset
|
52
|
+
@@connection.send(response)
|
53
|
+
end
|
54
|
+
return true
|
55
|
+
end
|
56
|
+
}
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
# Throw away all added routes from this router. Helpful for
|
61
|
+
# testing.
|
62
|
+
def purge_routes!
|
63
|
+
@routes = []
|
64
|
+
end
|
65
|
+
|
66
|
+
# Run the router DSL.
|
67
|
+
def draw(&block)
|
68
|
+
r = Router::DSL.new
|
69
|
+
r.instance_eval(&block)
|
70
|
+
r.routes.each do |route|
|
71
|
+
raise("Route lacks destination: #{route.inspect}") unless route.is_a?(Route)
|
72
|
+
end
|
73
|
+
@routes ||= []
|
74
|
+
@routes += r.routes
|
75
|
+
sort
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def sort
|
80
|
+
@routes.sort! { |r1,r2|
|
81
|
+
r2.priority <=> r1.priority
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Main router where all dispatchers shall register.
|
88
|
+
module CentralRouter
|
89
|
+
extend Router
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Route class which associate an XPATH match and a priority to a controller and an action
|
94
|
+
class Route
|
95
|
+
|
96
|
+
attr_accessor :priority, :controller, :action, :xpath
|
97
|
+
|
98
|
+
##
|
99
|
+
# Creates a new route
|
100
|
+
def initialize(params)
|
101
|
+
raise("No controller given for route") unless params["controller"]
|
102
|
+
raise("No action given for route") unless params["action"]
|
103
|
+
@priority = params["priority"] || 0
|
104
|
+
@xpath = params["xpath"]
|
105
|
+
@controller = Kernel.const_get("#{params["controller"].capitalize}Controller")
|
106
|
+
@action = params["action"]
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Checks that the route matches the stanzas and calls the the action on the controller
|
111
|
+
def accepts?(stanza)
|
112
|
+
stanza.xpath(@xpath, XpathHelper.new).first ? self : false
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Babylon
|
2
|
+
module Router
|
3
|
+
|
4
|
+
# Creates a simple DSL for stanza routing.
|
5
|
+
class DSL
|
6
|
+
attr_reader :routes
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@routes = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Match an xpath.
|
13
|
+
def xpath(path)
|
14
|
+
@routes << {"xpath" => path}
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set the priority of the last created route.
|
19
|
+
def priority(n)
|
20
|
+
set(:priority, n)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# Match a disco_info query.
|
25
|
+
def disco_info(node = nil)
|
26
|
+
disco_for(:info, node)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Match a disco_items query.
|
30
|
+
def disco_items(node = nil)
|
31
|
+
disco_for(:items, node)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Map a route to a specific controller and action.
|
35
|
+
def to(params)
|
36
|
+
set(:controller, params[:controller])
|
37
|
+
set(:action, params[:action])
|
38
|
+
# We now have all the properties we really need to create a route.
|
39
|
+
route = Route.new(@routes.pop)
|
40
|
+
@routes << route
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
# We do this magic, or crap depending on your perspective, because we don't know whether we're setting values on a
|
46
|
+
# Hash or a Route. We can't create the Route until we have a controller and action.
|
47
|
+
def set(property, value)
|
48
|
+
last = @routes.last
|
49
|
+
last[property.to_s] = value if last.is_a?(Hash)
|
50
|
+
last.send("#{property.to_s}=", value) if last.is_a?(Route)
|
51
|
+
end
|
52
|
+
|
53
|
+
def disco_for(type, node = nil)
|
54
|
+
str = "//iq[@type='get']/*[namespace(., 'query', 'http://jabber.org/protocol/disco##{type.to_s}')"
|
55
|
+
str += " and @node = '#{node}'" if node
|
56
|
+
str += "]"
|
57
|
+
xpath(str)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Babylon
|
2
|
+
|
3
|
+
##
|
4
|
+
# Runner is in charge of running the application.
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
##
|
8
|
+
# When run is called, it loads the configuration, the routes and add them into the router
|
9
|
+
# It then loads the models.
|
10
|
+
# Finally it starts the EventMachine and connect the ComponentConnection
|
11
|
+
# You can pass an additional block that will be called upon launching, when the eventmachine has been started.
|
12
|
+
def self.run(env)
|
13
|
+
|
14
|
+
Babylon.environment = env
|
15
|
+
|
16
|
+
# Starting the EventMachine
|
17
|
+
EventMachine.epoll
|
18
|
+
EventMachine.run do
|
19
|
+
|
20
|
+
# Requiring all models
|
21
|
+
Dir.glob('app/models/*.rb').each { |f| require f }
|
22
|
+
|
23
|
+
# Load the controllers
|
24
|
+
Dir.glob('app/controllers/*_controller.rb').each {|f| require f }
|
25
|
+
|
26
|
+
# Evaluate routes defined with the new DSL router.
|
27
|
+
CentralRouter.draw do
|
28
|
+
eval File.read("config/routes.rb")
|
29
|
+
end
|
30
|
+
|
31
|
+
config_file = File.open('config/config.yaml')
|
32
|
+
|
33
|
+
|
34
|
+
# Caching views in production mode.
|
35
|
+
if Babylon.environment == "production"
|
36
|
+
Babylon.cache_views
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
Babylon.config = YAML.load(config_file)[Babylon.environment]
|
41
|
+
|
42
|
+
params, on_connected = Babylon.config.merge({:on_stanza => Babylon::CentralRouter.method(:route)}), Babylon::CentralRouter.method(:connected)
|
43
|
+
|
44
|
+
case Babylon.config["application_type"]
|
45
|
+
when "client"
|
46
|
+
Babylon::ClientConnection.connect(params, &on_connected)
|
47
|
+
else # By default, we assume it's a component
|
48
|
+
Babylon::ComponentConnection.connect(params, &on_connected)
|
49
|
+
end
|
50
|
+
|
51
|
+
# And finally, let's allow the application to do all it wants to do after we started the EventMachine!
|
52
|
+
yield if block_given?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
module Babylon
|
2
|
+
|
3
|
+
##
|
4
|
+
# Connection Exception
|
5
|
+
class NotConnected < Exception; end
|
6
|
+
|
7
|
+
##
|
8
|
+
# xml-not-well-formed Exception
|
9
|
+
class XmlNotWellFormed < Exception; end
|
10
|
+
|
11
|
+
|
12
|
+
##
|
13
|
+
# This class is in charge of handling the network connection to the XMPP server.
|
14
|
+
class XmppConnection < EventMachine::Connection
|
15
|
+
|
16
|
+
attr_accessor :jid, :host, :port
|
17
|
+
|
18
|
+
##
|
19
|
+
# Connects the XmppConnection to the right host with the right port. I
|
20
|
+
# It passes itself (as handler) and the configuration
|
21
|
+
# This can very well be overwritten by subclasses.
|
22
|
+
def self.connect(params, &block)
|
23
|
+
Babylon.logger.debug("CONNECTING TO #{params["host"]}:#{params["port"]}") # Very low level Logging
|
24
|
+
EventMachine.connect(params["host"], params["port"], self, params.merge({:on_connection => block}))
|
25
|
+
end
|
26
|
+
|
27
|
+
def connection_completed
|
28
|
+
Babylon.logger.debug("CONNECTED") # Very low level Logging
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Called when the connection is terminated and stops the event loop
|
33
|
+
def unbind()
|
34
|
+
Babylon.logger.debug("DISCONNECTED") # Very low level Logging
|
35
|
+
EventMachine::stop_event_loop
|
36
|
+
raise NotConnected
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Instantiate the Handler (called internally by EventMachine) and attaches a new XmppParser
|
41
|
+
def initialize(params)
|
42
|
+
super()
|
43
|
+
@last_stanza_received = nil
|
44
|
+
@last_stanza_sent = nil
|
45
|
+
@jid = params["jid"]
|
46
|
+
@password = params["password"]
|
47
|
+
@host = params["host"]
|
48
|
+
@port = params["port"]
|
49
|
+
@stanza_callback = params[:on_stanza]
|
50
|
+
@connection_callback = params[:on_connection]
|
51
|
+
@parser = XmppParser.new(&method(:receive_stanza))
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Called when a full stanza has been received and returns it to the central router to be sent to the corresponding controller.
|
56
|
+
def receive_stanza(stanza)
|
57
|
+
@last_stanza_received = stanza
|
58
|
+
Babylon.logger.debug("PARSED : #{stanza.to_xml}")
|
59
|
+
# If not handled by subclass (for authentication)
|
60
|
+
case stanza.name
|
61
|
+
when "stream:error"
|
62
|
+
if stanza.at("xml-not-well-formed")
|
63
|
+
Babylon.logger.error("DISCONNECTED DUE TO MALFORMED STANZA : \n#{@last_stanza_sent}")
|
64
|
+
# <stream:error><xml-not-well-formed xmlns:xmlns="urn:ietf:params:xml:ns:xmpp-streams"/></stream:error>
|
65
|
+
raise XmlNotWellFormed
|
66
|
+
end
|
67
|
+
# In any case, we need to close the connection.
|
68
|
+
close_connection
|
69
|
+
else
|
70
|
+
@stanza_callback.call(stanza) if @stanza_callback
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Sends the Nokogiri::XML data (after converting to string) on the stream. It also appends the right "from" to be the component's JId if none has been mentionned. Eventually it displays this data for debugging purposes.
|
77
|
+
# This method also adds a "from" attribute to all stanza if it was ommited (the full jid) only if a "to" attribute is present. if not, we assume that we're speaking to the server and the server doesn't need a "from" to identify where the message is coming from.
|
78
|
+
def send(xml)
|
79
|
+
if xml.is_a? Nokogiri::XML::NodeSet
|
80
|
+
xml.each do |node|
|
81
|
+
send_node(node)
|
82
|
+
end
|
83
|
+
elsif xml.is_a? Nokogiri::XML::Node
|
84
|
+
send_node(xml)
|
85
|
+
else
|
86
|
+
# We try a cast into a string.
|
87
|
+
send_string("#{xml}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
##
|
94
|
+
# Sends a node on the "line".
|
95
|
+
def send_node(node)
|
96
|
+
@last_stanza_sent = node
|
97
|
+
node["from"] = jid if !node.attributes["from"] && node.attributes["to"]
|
98
|
+
send_string(node.to_xml)
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Sends a string on the line
|
103
|
+
def send_string(string)
|
104
|
+
Babylon.logger.debug("SENDING : #{string}")
|
105
|
+
send_data("#{string}")
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# receive_data is called when data is received. It is then passed to the parser.
|
110
|
+
def receive_data(data)
|
111
|
+
Babylon.logger.debug("RECEIVED : #{data}")
|
112
|
+
@parser.push(data)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# This is the XML SAX Parser that accepts "pushed" content
|
118
|
+
class XmppParser < Nokogiri::XML::SAX::Document
|
119
|
+
|
120
|
+
attr_accessor :elem, :doc
|
121
|
+
|
122
|
+
##
|
123
|
+
# Initialize the parser and adds the callback that will be called upon stanza completion
|
124
|
+
def initialize(&callback)
|
125
|
+
@callback = callback
|
126
|
+
super()
|
127
|
+
reset
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Resets the Pushed SAX Parser.
|
132
|
+
def reset
|
133
|
+
@parser = Nokogiri::XML::SAX::PushParser.new(self)
|
134
|
+
@doc = Nokogiri::XML::Document.new
|
135
|
+
@elem = nil
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Pushes the received data to the parser. The parser will then callback the document's methods (start_tag, end_tag... etc)
|
140
|
+
def push(data)
|
141
|
+
@parser << data
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Called when the document contains a CData block
|
146
|
+
def cdata_block(string)
|
147
|
+
cdata = Nokogiri::XML::CDATA.new(@doc, string)
|
148
|
+
@elem.add_child(cdata)
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Called when the document received in the stream is started
|
153
|
+
def start_document
|
154
|
+
@doc = Nokogiri::XML::Document.new
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Adds characters to the current element (being parsed)
|
159
|
+
def characters(string)
|
160
|
+
@last_text_elem ||= @elem
|
161
|
+
@last_text = @last_text ? @last_text + string : string
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Instantiate a new current Element, adds the corresponding attributes and namespaces
|
166
|
+
# The new element is eventually added to a parent element (if present).
|
167
|
+
# If this element is the first element (the root of the document), then instead of adding it to a parent, we add it to the document itself. In this case, the current element will not be terminated, so we activate the callback immediately.
|
168
|
+
def start_element(qname, attributes = [])
|
169
|
+
e = Nokogiri::XML::Element.new(qname, @doc)
|
170
|
+
add_namespaces_and_attributes_to_node(attributes, e)
|
171
|
+
|
172
|
+
# Adding the newly created element to the @elem that is being parsed, or, if no element is being parsed, then we set the @root and the @elem to be this newly created element.
|
173
|
+
@elem = @elem ? @elem.add_child(e) : (@root = e)
|
174
|
+
|
175
|
+
if @elem.name == "stream:stream"
|
176
|
+
# Should be called only for stream:stream.
|
177
|
+
# We re-initialize the document and set its root to be the doc.
|
178
|
+
# Also, we activate the callback since this element will never end.
|
179
|
+
@doc = Nokogiri::XML::Document.new
|
180
|
+
@doc.root = @root = @elem
|
181
|
+
@callback.call(@elem)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
##
|
186
|
+
# Terminates the current element and calls the callback
|
187
|
+
def end_element(name)
|
188
|
+
if @last_text_elem
|
189
|
+
@elem.add_child(Nokogiri::XML::Text.new(@last_text, @doc))
|
190
|
+
@last_text_elem = nil
|
191
|
+
@last_text = nil
|
192
|
+
end
|
193
|
+
if @elem
|
194
|
+
if @elem.parent == @root
|
195
|
+
@callback.call(@elem)
|
196
|
+
# And we also need to remove @elem from its tree
|
197
|
+
@elem.unlink
|
198
|
+
# And the current elem is the next sibling or the root
|
199
|
+
@elem = @root
|
200
|
+
else
|
201
|
+
@elem = @elem.parent
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
##
|
209
|
+
# Adds namespaces and attributes. Nokogiri passes them as a array of [name, value, name, value]...
|
210
|
+
def add_namespaces_and_attributes_to_node(attrs, node)
|
211
|
+
(attrs.size / 2).times do |i|
|
212
|
+
name, value = attrs[2 * i], attrs[2 * i + 1]
|
213
|
+
if name =~ /xmlns/
|
214
|
+
node.add_namespace(name, value)
|
215
|
+
else
|
216
|
+
node.set_attribute name, value
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Babylon
|
2
|
+
|
3
|
+
# Custom XPath functions for stanza-routing.
|
4
|
+
class XpathHelper
|
5
|
+
|
6
|
+
# Match nodes of the given name with the given namespace URI.
|
7
|
+
def namespace(set, name, nsuri)
|
8
|
+
set.find_all.each do |n|
|
9
|
+
n.name == name && n.namespaces.values.include?(nsuri)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
You can define you class models here. It is totally ok to reuse your ActiveRecord from another application!
|
@@ -0,0 +1,12 @@
|
|
1
|
+
= Babylon::Base::View
|
2
|
+
|
3
|
+
== Usage
|
4
|
+
|
5
|
+
Please see Babylon Rdoc. Put all the views related to controller MyController into app/views/my/...
|
6
|
+
This file are Xml Builder Files (see Nokogiri Documentation for any doubt).
|
7
|
+
|
8
|
+
== Example
|
9
|
+
|
10
|
+
self.message(:to => to, :from => from, :type => :chat) do
|
11
|
+
self.body(body) // Same as self.send(:body, body) (
|
12
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "babylon"
|
3
|
+
require File.dirname(__FILE__)+"/dependencies"
|
4
|
+
|
5
|
+
# Start the App
|
6
|
+
Babylon::Runner::run(ARGV[0] || "development") do
|
7
|
+
# And the initializers, too. This is done here since some initializers might need EventMachine to be started.
|
8
|
+
Dir[File.join(File.dirname(__FILE__), '/initializers/*.rb')].each {|f| require f }
|
9
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# This contains the global configuration of your component.
|
2
|
+
# environment:
|
3
|
+
# jid: your.component.jid
|
4
|
+
# password: your.component.password
|
5
|
+
# host: host on which the XMPP server is running
|
6
|
+
# port: port to which your component should connect
|
7
|
+
# application_type: client | component (by default it is component and we strongly discourage any client application in production)
|
8
|
+
|
9
|
+
development:
|
10
|
+
jid: component.server.com
|
11
|
+
password: password
|
12
|
+
host: localhost
|
13
|
+
port: 5278
|
14
|
+
application_type: client
|
15
|
+
|
16
|
+
test:
|
17
|
+
jid: component.server.com
|
18
|
+
password: password
|
19
|
+
host: localhost
|
20
|
+
port: 5278
|
21
|
+
|
22
|
+
production:
|
23
|
+
jid: component.server.com
|
24
|
+
password: password
|
25
|
+
host: localhost
|
26
|
+
port: 5278
|
@@ -0,0 +1 @@
|
|
1
|
+
Place any application-specific code that needs to be run during initialization in this directory.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Routes require an xpath against which to match, and a controller/action pair to which to map.
|
2
|
+
#
|
3
|
+
# xpath("//message[@type = 'chat']"
|
4
|
+
# ).to(:controller => "message", :action => "receive")
|
5
|
+
#
|
6
|
+
# Routes can be assigned priorities. The highest priority executes first, and the default priority is 0.
|
7
|
+
#
|
8
|
+
# xpath("//message[@type = 'chat']"
|
9
|
+
# ).to(:controller => "message", :action => "priority"
|
10
|
+
# ).priority(5000000)
|
11
|
+
#
|
12
|
+
# It is not possible to easily check for namespace URI equivalence in xpath, but the following helper function was added.
|
13
|
+
#
|
14
|
+
# xpath("//iq[@type='get']/*[namespace(., 'query', 'http://jabber.org/protocol/disco#info')]"
|
15
|
+
# ).to(:controller => "discovery", :action => "services")
|
16
|
+
#
|
17
|
+
# That syntax is ugly out of necessity. But, relax, you're using Ruby.
|
18
|
+
#
|
19
|
+
# There are a few helper methods for generating xpaths. The following is equivalent to the above example:
|
20
|
+
#
|
21
|
+
# disco_info.to(:controller => "discovery", :action => "services")
|
22
|
+
#
|
23
|
+
# See lib/babylon/router/dsl.rb for more helpers.
|
24
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: julien51-babylon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- julien Genestoux
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-03-
|
12
|
+
date: 2009-03-22 00:00:00 -07:00
|
13
13
|
default_executable: babylon
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -24,8 +24,27 @@ extra_rdoc_files:
|
|
24
24
|
- LICENSE
|
25
25
|
files:
|
26
26
|
- bin/babylon
|
27
|
-
-
|
27
|
+
- lib/babylon.rb
|
28
|
+
- lib/babylon/base/controller.rb
|
29
|
+
- lib/babylon/base/view.rb
|
30
|
+
- lib/babylon/client_connection.rb
|
31
|
+
- lib/babylon/component_connection.rb
|
32
|
+
- lib/babylon/router/dsl.rb
|
33
|
+
- lib/babylon/router.rb
|
34
|
+
- lib/babylon/runner.rb
|
35
|
+
- lib/babylon/xmpp_connection.rb
|
36
|
+
- lib/babylon/xpath_helper.rb
|
28
37
|
- LICENSE
|
38
|
+
- Rakefile
|
39
|
+
- README.rdoc
|
40
|
+
- templates/babylon/app/controllers/README.rdoc
|
41
|
+
- templates/babylon/app/models/README.rdoc
|
42
|
+
- templates/babylon/app/views/README.rdoc
|
43
|
+
- templates/babylon/config/boot.rb
|
44
|
+
- templates/babylon/config/config.yaml
|
45
|
+
- templates/babylon/config/dependencies.rb
|
46
|
+
- templates/babylon/config/routes.rb
|
47
|
+
- templates/babylon/config/initializers/README.rdoc
|
29
48
|
has_rdoc: true
|
30
49
|
homepage: http://github.com/julien51/babylon
|
31
50
|
post_install_message:
|