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 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 "Syntax : $> babylon app_name "
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,13 @@
1
+ = Babylon::Base::Controller
2
+
3
+ == Usage :
4
+
5
+ Please see Babylon rdoc.
6
+
7
+ == Example
8
+
9
+ class MyController < Babylon::Base::Controller
10
+ def my_action
11
+ // Do something great!
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,2 @@
1
+ # Require any application-specific dependencies here.
2
+
@@ -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.7
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-18 00:00:00 -07:00
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
- - README.rdoc
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: