├── .hound.yml
├── .rspec
├── lib
├── blather
│ ├── version.rb
│ ├── stanza
│ │ ├── pubsub
│ │ │ ├── errors.rb
│ │ │ ├── subscribe.rb
│ │ │ ├── create.rb
│ │ │ ├── affiliations.rb
│ │ │ ├── unsubscribe.rb
│ │ │ ├── subscriptions.rb
│ │ │ ├── retract.rb
│ │ │ ├── items.rb
│ │ │ ├── publish.rb
│ │ │ ├── event.rb
│ │ │ └── subscription.rb
│ │ ├── disco.rb
│ │ ├── presence
│ │ │ ├── muc.rb
│ │ │ ├── c.rb
│ │ │ ├── subscription.rb
│ │ │ └── muc_user.rb
│ │ ├── muc
│ │ │ └── muc_user_base.rb
│ │ ├── iq
│ │ │ ├── ping.rb
│ │ │ ├── query.rb
│ │ │ ├── ibr.rb
│ │ │ └── ibb.rb
│ │ ├── pubsub_owner
│ │ │ ├── purge.rb
│ │ │ └── delete.rb
│ │ ├── pubsub_owner.rb
│ │ ├── message
│ │ │ └── muc_user.rb
│ │ └── pubsub.rb
│ ├── core_ext
│ │ ├── ipaddr.rb
│ │ └── eventmachine.rb
│ ├── stream
│ │ ├── features
│ │ │ ├── tls.rb
│ │ │ ├── session.rb
│ │ │ ├── register.rb
│ │ │ └── resource.rb
│ │ ├── client.rb
│ │ ├── component.rb
│ │ ├── features.rb
│ │ └── parser.rb
│ ├── errors
│ │ ├── sasl_error.rb
│ │ ├── stream_error.rb
│ │ └── stanza_error.rb
│ ├── errors.rb
│ ├── cert_store.rb
│ ├── file_transfer
│ │ ├── ibb.rb
│ │ └── s5b.rb
│ ├── client.rb
│ ├── roster.rb
│ ├── xmpp_node.rb
│ └── file_transfer.rb
└── blather.rb
├── yard
└── templates
│ └── default
│ └── class
│ ├── text
│ └── handlers.erb
│ ├── setup.rb
│ └── html
│ └── handlers.erb
├── Gemfile
├── spec
├── support
│ └── mock_server.rb
├── spec_helper.rb
├── blather
│ ├── stream
│ │ ├── ssl_spec.rb
│ │ └── component_spec.rb
│ ├── errors
│ │ ├── sasl_error_spec.rb
│ │ └── stream_error_spec.rb
│ ├── errors_spec.rb
│ ├── stanza
│ │ ├── pubsub_owner_spec.rb
│ │ ├── presence
│ │ │ ├── muc_spec.rb
│ │ │ ├── c_spec.rb
│ │ │ ├── muc_user_spec.rb
│ │ │ └── subscription_spec.rb
│ │ ├── iq
│ │ │ ├── ping_spec.rb
│ │ │ ├── s5b_spec.rb
│ │ │ ├── query_spec.rb
│ │ │ ├── si_spec.rb
│ │ │ ├── vcard_spec.rb
│ │ │ └── ibb_spec.rb
│ │ ├── pubsub_owner
│ │ │ ├── purge_spec.rb
│ │ │ └── delete_spec.rb
│ │ ├── pubsub
│ │ │ ├── create_spec.rb
│ │ │ ├── affiliations_spec.rb
│ │ │ ├── subscribe_spec.rb
│ │ │ ├── retract_spec.rb
│ │ │ ├── subscriptions_spec.rb
│ │ │ ├── items_spec.rb
│ │ │ ├── publish_spec.rb
│ │ │ ├── unsubscribe_spec.rb
│ │ │ └── event_spec.rb
│ │ ├── iq_spec.rb
│ │ └── pubsub_spec.rb
│ ├── xmpp_node_spec.rb
│ ├── jid_spec.rb
│ ├── roster_spec.rb
│ └── roster_item_spec.rb
├── fixtures
│ └── certificate.crt
└── blather_spec.rb
├── Guardfile
├── .gitignore
├── examples
├── execute.rb
├── rosterprint.rb
├── echo.rb
├── trusted_echo.rb
├── MUC_echo.rb
├── stream_only.rb
├── ping_pong.rb
├── xmpp4r
│ └── echo.rb
├── certs
│ └── README
└── print_hierarchy.rb
├── .travis.yml
├── Rakefile
├── LICENSE
└── blather.gemspec
/.hound.yml:
--------------------------------------------------------------------------------
1 | LineLength:
2 | Enabled: false
3 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --colour
3 | --tty
4 |
--------------------------------------------------------------------------------
/lib/blather/version.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | VERSION = '2.0.0'
3 | end
4 |
--------------------------------------------------------------------------------
/yard/templates/default/class/text/handlers.erb:
--------------------------------------------------------------------------------
1 | <%= indent(wrap("Handler Stack: #{@handler_stack * ', '}")) %>]
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gemspec
4 |
5 | gem 'activesupport', '~> 3.0' if RUBY_VERSION == "1.9.2"
6 |
--------------------------------------------------------------------------------
/spec/support/mock_server.rb:
--------------------------------------------------------------------------------
1 | class MockServer; end
2 |
3 | module ServerMock
4 | def receive_data(data)
5 | @server ||= MockServer.new
6 | @server.receive_data data, self
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard 'rspec', cmd: 'bundle exec rspec' do
2 | watch(%r{^spec/.+_spec\.rb$})
3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4 | watch('spec/spec_helper.rb') { "spec/" }
5 | end
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg
2 | Manifest
3 | doc
4 | coverage
5 | *.bundle
6 | Gemfile.lock
7 | mkmf.log
8 | *.o
9 | Makefile
10 | rdoc
11 | *.gem
12 | *.log
13 | *.pid
14 | notes
15 | .yardoc
16 | *.rbc
17 | vendor
18 | .rvmrc
19 | .rbx
20 | tmp
21 |
--------------------------------------------------------------------------------
/yard/templates/default/class/setup.rb:
--------------------------------------------------------------------------------
1 | def init
2 | super
3 | sections.place(:handlers).after(:box_info)
4 | end
5 |
6 | def handlers
7 | @handler_stack = object.inheritance_tree.map { |o| o.tag(:handler).name if (o.respond_to?(:tag) && o.tag(:handler)) }.compact
8 | return if @handler_stack.empty?
9 | erb(:handlers)
10 | end
11 |
--------------------------------------------------------------------------------
/examples/execute.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'rubygems'
4 | require 'blather/client'
5 |
6 | message :chat?, :body => 'exit' do |m|
7 | say m.from, 'Exiting ...'
8 | shutdown
9 | end
10 |
11 | message :chat?, :body do |m|
12 | begin
13 | say m.from, eval(m.body)
14 | rescue => e
15 | say m.from, e.inspect
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.3.7
4 | - 2.4.4
5 | - jruby-9.1.17.0
6 | - rbx-3.105
7 | - ruby-head
8 | jdk:
9 | - openjdk8 # for jruby
10 | matrix:
11 | allow_failures:
12 | - rvm: ruby-head
13 | - rvm: rbx-3.105
14 | before_install:
15 | - gem install bundler
16 | notifications:
17 | irc: "irc.freenode.org#adhearsion"
18 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/errors.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 |
4 | # # PusSub Error Stanza
5 | #
6 | # @private
7 | class PubSubErrors < PubSub
8 | def node
9 | read_attr :node
10 | end
11 |
12 | def node=(node)
13 | write_attr :node, node
14 | end
15 | end # PubSubErrors
16 |
17 | end # Stanza
18 | end # Blather
19 |
--------------------------------------------------------------------------------
/examples/rosterprint.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # Prints out each roster entry
4 |
5 | require 'rubygems'
6 | require 'blather/client'
7 |
8 | when_ready do
9 | my_roster.grouped.each do |group, items|
10 | puts "#{'*'*3} #{group || 'Ungrouped'} #{'*'*3}"
11 | items.each { |item| puts "- #{item.name} (#{item.jid})" }
12 | puts
13 | end
14 | shutdown
15 | end
16 |
--------------------------------------------------------------------------------
/examples/echo.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'rubygems'
4 | require 'blather/client'
5 |
6 | when_ready { puts "Connected ! send messages to #{jid.stripped}." }
7 |
8 | subscription :request? do |s|
9 | write_to_stream s.approve!
10 | end
11 |
12 | message :chat?, :body => 'exit' do |m|
13 | say m.from, 'Exiting ...'
14 | shutdown
15 | end
16 |
17 | message :chat?, :body do |m|
18 | say m.from, "You sent: #{m.body}"
19 | end
20 |
--------------------------------------------------------------------------------
/lib/blather/core_ext/ipaddr.rb:
--------------------------------------------------------------------------------
1 | # @private
2 | class IPAddr
3 | PrivateRanges = [
4 | IPAddr.new("10.0.0.0/8"),
5 | IPAddr.new("172.16.0.0/12"),
6 | IPAddr.new("192.168.0.0/16")
7 | ]
8 |
9 | def private?
10 | return false unless self.ipv4?
11 | PrivateRanges.each do |ipr|
12 | return true if ipr.include?(self)
13 | end
14 | return false
15 | end
16 |
17 | def public?
18 | !private?
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/examples/trusted_echo.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'rubygems'
4 | require 'blather/client'
5 |
6 | setup 'chris@vines.local', 'test', 'vines.local', 5222, "./certs"
7 |
8 | when_ready { puts "Connected ! send messages to #{jid.stripped}." }
9 |
10 | subscription :request? do |s|
11 | write_to_stream s.approve!
12 | end
13 |
14 | message :chat?, :body => 'exit' do |m|
15 | say m.from, 'Exiting ...'
16 | shutdown
17 | end
18 |
19 | message :chat?, :body do |m|
20 | say m.from, "You sent: #{m.body}"
21 | end
22 |
--------------------------------------------------------------------------------
/examples/MUC_echo.rb:
--------------------------------------------------------------------------------
1 | require 'blather/client/dsl'
2 |
3 | module MUC
4 | extend Blather::DSL
5 | when_ready do
6 | puts "Connected ! send messages to #{jid.stripped}."
7 | join 'room_name', 'nick_name'
8 | end
9 |
10 | message :groupchat?, :body, proc { |m| m.from != jid.stripped }, delay: nil do |m|
11 | echo = Blather::Stanza::Message.new
12 | echo.to = room
13 | echo.body = m.body
14 | echo.type = 'groupchat'
15 | client.write echo
16 | end
17 | end
18 | MUC.setup 'username', 'password'
19 | EM.run { MUC.run }
20 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # -*- ruby -*-
2 | ENV['RUBY_FLAGS'] = "-I#{%w(lib ext bin spec).join(File::PATH_SEPARATOR)}"
3 |
4 | require 'rubygems'
5 | require 'bundler/gem_tasks'
6 | require 'bundler/setup'
7 |
8 | task :default => :spec
9 | task :test => :spec
10 |
11 | require 'rspec/core/rake_task'
12 | RSpec::Core::RakeTask.new
13 |
14 | require 'yard'
15 | YARD::Tags::Library.define_tag 'Blather handler', :handler, :with_name
16 | YARD::Templates::Engine.register_template_path 'yard/templates'
17 |
18 | YARD::Rake::YardocTask.new(:doc) do |t|
19 | t.options = ['--no-private', '-m', 'markdown', '-o', './doc/public/yard']
20 | end
21 |
--------------------------------------------------------------------------------
/yard/templates/default/class/html/handlers.erb:
--------------------------------------------------------------------------------
1 |
15 |
16 | Handler Stack:
17 | [<%= @handler_stack * ', ' %>]
18 |
19 |
--------------------------------------------------------------------------------
/examples/stream_only.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'rubygems'
4 | require 'blather'
5 |
6 | trap(:INT) { EM.stop }
7 | trap(:TERM) { EM.stop }
8 | EM.run do
9 | Blather::Stream::Client.start(Class.new {
10 | attr_accessor :jid
11 |
12 | def post_init(stream, jid = nil)
13 | @stream = stream
14 | self.jid = jid
15 |
16 | @stream.send_data Blather::Stanza::Presence::Status.new
17 | puts "Stream started!"
18 | end
19 |
20 | def receive_data(stanza)
21 | @stream.send_data stanza.reply!
22 | end
23 |
24 | def unbind
25 | puts "Stream ended!"
26 | end
27 | }.new, 'echo@jabber.local', 'echo')
28 | end
29 |
--------------------------------------------------------------------------------
/lib/blather/stream/features/tls.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 |
4 | # @private
5 | class TLS < Features
6 | class TLSFailure < BlatherError
7 | register :tls_failure
8 | end
9 |
10 | TLS_NS = 'urn:ietf:params:xml:ns:xmpp-tls'.freeze
11 | register TLS_NS
12 |
13 | def receive_data(stanza)
14 | case stanza.element_name
15 | when 'starttls'
16 | @stream.send ""
17 | when 'proceed'
18 | @stream.start_tls(:verify_peer => true)
19 | @stream.start
20 | else
21 | fail! TLSFailure.new
22 | end
23 | end
24 |
25 | end #TLS
26 |
27 | end #Stream
28 | end #Blather
29 |
--------------------------------------------------------------------------------
/lib/blather/stream/client.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 |
4 | # @private
5 | class Client < Stream
6 | LANG = 'en'
7 | VERSION = '1.0'
8 | NAMESPACE = 'jabber:client'
9 |
10 | def start
11 | @parser = Parser.new self
12 | send ""
13 | end
14 |
15 | def send(stanza)
16 | stanza.from = self.jid if stanza.is_a?(Stanza) && !stanza.from.nil?
17 | super stanza
18 | end
19 |
20 | def cleanup
21 | @parser.finish if @parser
22 | super
23 | end
24 | end #Client
25 |
26 | end #Stream
27 | end #Blather
28 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'blather'
2 | require 'countdownlatch'
3 |
4 | Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
5 |
6 | RSpec.configure do |config|
7 | config.mock_with :mocha
8 | config.filter_run :focus => true
9 | config.run_all_when_everything_filtered = true
10 |
11 | config.before(:each) do
12 | EM.stubs(:schedule).yields
13 | EM.stubs(:defer).yields
14 | Blather::Stream::Parser.debug = true
15 | Blather.logger = Logger.new($stdout).tap { |logger| logger.level = Logger::DEBUG }
16 | end
17 | end
18 |
19 | def parse_stanza(xml)
20 | Nokogiri::XML.parse xml
21 | end
22 |
23 | def jruby?
24 | RUBY_PLATFORM =~ /java/
25 | end
26 |
--------------------------------------------------------------------------------
/spec/blather/stream/ssl_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::CertStore do
4 | let(:cert_dir) { File.expand_path '../../fixtures', File.dirname(__FILE__) }
5 | let(:cert_path) { File.join cert_dir, 'certificate.crt' }
6 | let(:cert) { File.read cert_path }
7 |
8 | subject do
9 | Blather::CertStore.new cert_dir
10 | end
11 |
12 | it 'can verify valid cert' do
13 | expect(subject.trusted?(cert)).to be true
14 | end
15 |
16 | it 'can verify invalid cert' do
17 | expect(subject.trusted?('foo bar baz')).to be_nil
18 | end
19 |
20 | it 'cannot verify when the cert authority is not trusted' do
21 | @store = Blather::CertStore.new("../")
22 | expect(@store.trusted?(cert)).to be false
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/blather/stanza/disco.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 |
4 | # # Disco Base class
5 | #
6 | # Use Blather::Stanza::DiscoInfo or Blather::Stanza::DiscoItems
7 | class Disco < Iq::Query
8 |
9 | # Get the name of the node
10 | #
11 | # @return [String] the node name
12 | def node
13 | query[:node]
14 | end
15 |
16 | # Set the name of the node
17 | #
18 | # @param [#to_s] node the new node name
19 | def node=(node)
20 | query[:node] = node
21 | end
22 |
23 | # Compare two Disco objects by name, type and category
24 | # @param [Disco] o the Identity object to compare against
25 | # @return [true, false]
26 | def eql?(o, *fields)
27 | super o, *(fields + [:node])
28 | end
29 | end
30 |
31 | end # Stanza
32 | end # Blather
33 |
--------------------------------------------------------------------------------
/lib/blather/stanza/presence/muc.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class Presence
4 |
5 | class MUC < Status
6 | register :muc_join, :x, "http://jabber.org/protocol/muc"
7 |
8 | def self.new(*args)
9 | new_node = super
10 | new_node.muc
11 | new_node
12 | end
13 |
14 | module InstanceMethods
15 | def inherit(node)
16 | muc.remove
17 | super
18 | self
19 | end
20 |
21 | def muc
22 | unless muc = find_first('ns:x', :ns => MUC.registered_ns)
23 | self << (muc = XMPPNode.new('x', self.document))
24 | muc.namespace = self.class.registered_ns
25 | end
26 | muc
27 | end
28 | end
29 |
30 | include InstanceMethods
31 | end # MUC
32 |
33 | end # Presence
34 | end # Stanza
35 | end # Blather
36 |
--------------------------------------------------------------------------------
/spec/fixtures/certificate.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICAzCCAWwCCQDvLJRwh2NPnjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJH
3 | QjETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0
4 | cyBQdHkgTHRkMCAXDTEyMTIyMDAxMjgyOVoYDzI4MzQwNTA1MDEyODI5WjBFMQsw
5 | CQYDVQQGEwJHQjETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJu
6 | ZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDj
7 | EoRb/MYNjHrxq6cu6TVyM4hcimy5WWy369BvarONxpmHlrvDUiMQoZNBxyV//hZH
8 | Y3JSHrNxMCxU1vShghNZzNEZg45UBWMs3LS/IHnFvf+4gYbffEJCGMhjyx4zAxRA
9 | gegDAYIeGHtWAA6SIe4SIzsMtAJC0Nf8H3AZJdjvSwIDAQABMA0GCSqGSIb3DQEB
10 | BQUAA4GBAGkyf9MlpCesU7jxiZbI3jIdCEMiG5XsGp0c+nELufcsus4MySN0xZTt
11 | zvBBvWLpbv8zkxkw+kamvksObslx4G6hg1RvKP/Zdjue5mVnUTNKlXu/wQOTVPNo
12 | V3iBcJUwe3uIIlt2szA6o5BRt29XSLiNIJvFhAHCVg+3VioXChRr
13 | -----END CERTIFICATE-----
14 |
--------------------------------------------------------------------------------
/examples/ping_pong.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'blather/client/dsl'
3 | $stdout.sync = true
4 |
5 | module Ping
6 | extend Blather::DSL
7 | def self.run; client.run; end
8 |
9 | setup 'ping@your.jabber.server', 'password'
10 |
11 | status :from => /pong@your\.jabber\.server/ do |s|
12 | puts "serve!"
13 | say s.from, 'ping'
14 | end
15 |
16 | message :chat?, :body => 'pong' do |m|
17 | puts "ping!"
18 | say m.from, 'ping'
19 | end
20 | end
21 |
22 | module Pong
23 | extend Blather::DSL
24 | def self.run; client.run; end
25 |
26 | setup 'pong@your.jabber.server', 'password'
27 | message :chat?, :body => 'ping' do |m|
28 | puts "pong!"
29 | say m.from, 'pong'
30 | end
31 | end
32 |
33 | trap(:INT) { EM.stop }
34 | trap(:TERM) { EM.stop }
35 | EM.run do
36 | Ping.run
37 | Pong.run
38 | end
39 |
--------------------------------------------------------------------------------
/spec/blather_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather do
4 |
5 | describe "while accessing to Logger object" do
6 | it "should return a Logger instance" do
7 | expect(Blather.logger).to be_instance_of Logger
8 | end
9 | end
10 |
11 | describe "while using the log method" do
12 | after do
13 | Blather.default_log_level = :debug
14 | end
15 |
16 | it "should forward to debug by default" do
17 | Blather.logger.expects(:debug).with("foo bar").once
18 | Blather.log "foo bar"
19 | end
20 |
21 | %w.each do |val|
22 | it "should forward to #{val} if configured that default level" do
23 | Blather.logger.expects(val.to_sym).with("foo bar").once
24 | Blather.default_log_level = val.to_sym
25 | Blather.log "foo bar"
26 | end
27 | end
28 |
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/examples/xmpp4r/echo.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | # This bot will reply to every message it receives. To end the game, send 'exit'
4 |
5 | require 'rubygems'
6 | require 'xmpp4r/client'
7 | include Jabber
8 |
9 | # settings
10 | if ARGV.length != 2
11 | puts "Run with ./echo_thread.rb user@server/resource password"
12 | exit 1
13 | end
14 | myJID = JID.new(ARGV[0])
15 | myPassword = ARGV[1]
16 | cl = Client.new(myJID)
17 | cl.connect
18 | cl.auth(myPassword)
19 | cl.send(Presence.new)
20 | puts "Connected ! send messages to #{myJID.strip.to_s}."
21 | mainthread = Thread.current
22 | cl.add_message_callback do |m|
23 | if m.type != :error
24 | m2 = Message.new(m.from, "You sent: #{m.body}")
25 | m2.type = m.type
26 | cl.send(m2)
27 | if m.body == 'exit'
28 | m2 = Message.new(m.from, "Exiting ...")
29 | m2.type = m.type
30 | cl.send(m2)
31 | mainthread.wakeup
32 | end
33 | end
34 | end
35 | Thread.stop
36 | cl.close
37 |
--------------------------------------------------------------------------------
/lib/blather/stream/component.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 |
4 | # @private
5 | class Component < Stream
6 | NAMESPACE = 'jabber:component:accept'
7 |
8 | def receive(node) # :nodoc:
9 | if node.element_name == 'handshake'
10 | ready!
11 | else
12 | super
13 | end
14 |
15 | if node.document.find_first('/stream:stream[not(stream:error)]', :xmlns => NAMESPACE, :stream => STREAM_NS)
16 | send "#{Digest::SHA1.hexdigest(node['id']+@password)}"
17 | end
18 | end
19 |
20 | def send(stanza)
21 | stanza.from ||= self.jid if stanza.respond_to?(:from) && stanza.respond_to?(:from=)
22 | super stanza
23 | end
24 |
25 | def start
26 | @parser = Parser.new self
27 | send ""
28 | end
29 |
30 | def cleanup
31 | @parser.finish if @parser
32 | super
33 | end
34 | end #Client
35 |
36 | end #Stream
37 | end #Blather
38 |
--------------------------------------------------------------------------------
/lib/blather/errors/sasl_error.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 |
3 | # General SASL Errors
4 | # Check #name for the error name
5 | #
6 | # @handler :sasl_error
7 | class SASLError < BlatherError
8 | # @private
9 | SASL_ERR_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'
10 |
11 | class_attribute :err_name
12 | # @private
13 | @@registrations = {}
14 |
15 | register :sasl_error
16 |
17 | # Import the stanza
18 | #
19 | # @param [Blather::XMPPNode] node the error node
20 | # @return [Blather::SASLError]
21 | def self.import(node)
22 | self.new node
23 | end
24 |
25 | # Create a new SASLError
26 | #
27 | # @param [Blather::XMPPNode] node the error node
28 | def initialize(node)
29 | super()
30 | @node = node
31 | end
32 |
33 | # The actual error name
34 | #
35 | # @return [Symbol] a symbol representing the error name
36 | def name
37 | if @node
38 | name = @node.find_first('ns:*', :ns => SASL_ERR_NS).element_name
39 | name.gsub('-', '_').to_sym
40 | end
41 | end
42 | end # SASLError
43 |
44 | end # Blather
45 |
--------------------------------------------------------------------------------
/spec/blather/errors/sasl_error_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def sasl_error_node(err_name = 'aborted')
4 | node = Blather::XMPPNode.new 'failure'
5 | node.namespace = Blather::SASLError::SASL_ERR_NS
6 |
7 | node << Blather::XMPPNode.new(err_name, node.document)
8 | node
9 | end
10 |
11 | describe Blather::SASLError do
12 | it 'can import a node' do
13 | expect(Blather::SASLError).to respond_to :import
14 | e = Blather::SASLError.import sasl_error_node
15 | expect(e).to be_kind_of Blather::SASLError
16 | end
17 |
18 | describe 'each XMPP SASL error type' do
19 | %w[ aborted
20 | incorrect-encoding
21 | invalid-authzid
22 | invalid-mechanism
23 | mechanism-too-weak
24 | not-authorized
25 | temporary-auth-failure
26 | ].each do |error_type|
27 | it "handles the name for #{error_type}" do
28 | e = Blather::SASLError.import sasl_error_node(error_type)
29 | expect(e.name).to eq(error_type.gsub('-','_').to_sym)
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/blather/errors_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::BlatherError do
4 | it 'is handled by :error' do
5 | expect(Blather::BlatherError.new.handler_hierarchy).to eq([:error])
6 | end
7 | end
8 |
9 | describe 'Blather::ParseError' do
10 | before { @error = Blather::ParseError.new('"') }
11 |
12 | it 'is registers with the handler hierarchy' do
13 | expect(@error.handler_hierarchy).to eq([:parse_error, :error])
14 | end
15 |
16 | it 'contains the error message' do
17 | expect(@error).to respond_to :message
18 | expect(@error.message).to eq('"')
19 | end
20 | end
21 |
22 | describe 'Blather::UnknownResponse' do
23 | before { @error = Blather::UnknownResponse.new(Blather::XMPPNode.new('foo-bar')) }
24 |
25 | it 'is registers with the handler hierarchy' do
26 | expect(@error.handler_hierarchy).to eq([:unknown_response_error, :error])
27 | end
28 |
29 | it 'holds on to a copy of the failure node' do
30 | expect(@error).to respond_to :node
31 | expect(@error.node.element_name).to eq('foo-bar')
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/blather/stream/features/session.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 |
4 | # @private
5 | class Session < Features
6 | SESSION_NS = 'urn:ietf:params:xml:ns:xmpp-session'.freeze
7 | register SESSION_NS
8 |
9 | def initialize(stream, succeed, fail)
10 | super
11 | @to = @stream.jid.domain
12 | end
13 |
14 | def receive_data(stanza)
15 | @node = stanza
16 | case stanza.element_name
17 | when 'session' then session
18 | when 'iq' then check_response
19 | else fail!(UnknownResponse.new(stanza))
20 | end
21 | end
22 |
23 | private
24 | def check_response
25 | if @node[:type] == 'result'
26 | succeed!
27 | else
28 | fail!(StanzaError.import(@node))
29 | end
30 | end
31 |
32 | ##
33 | # Send a start session command
34 | def session
35 | response = Stanza::Iq.new :set
36 | response.to = @to
37 | response << (sess = XMPPNode.new('session', response.document))
38 | sess.namespace = SESSION_NS
39 |
40 | @stream.send response
41 | end
42 | end
43 |
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Blather
2 |
3 | Copyright (c) 2012 Adhearsion Foundation Inc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/lib/blather/stream/features/register.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 | class Register < Features
4 | REGISTER_NS = "http://jabber.org/features/iq-register".freeze
5 |
6 | register REGISTER_NS
7 |
8 | def initialize(stream, succeed, fail)
9 | super
10 | @jid = @stream.jid
11 | @pass = @stream.password
12 | end
13 |
14 | def receive_data(stanza)
15 | error_node = stanza.xpath("//error").first
16 |
17 | if error_node
18 | fail!(BlatherError.new(stanza))
19 | elsif stanza['type'] == 'result' && (stanza.content.empty? || stanza.children.find { |v| v.element_name == "query" })
20 | succeed!
21 | else
22 | @stream.send register_query
23 | end
24 | end
25 |
26 | def register_query
27 | node = Blather::Stanza::Iq::Query.new(:set)
28 | query_node = node.xpath('//query').first
29 | query_node['xmlns'] = 'jabber:iq:register'
30 | Nokogiri::XML::Builder.with(query_node) do |xml|
31 | xml.username @jid.node
32 | xml.password @pass
33 | end
34 | node
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/examples/certs/README:
--------------------------------------------------------------------------------
1 | The certs/ directory contains the TLS certificates required for encrypting
2 | client to server XMPP connections.
3 |
4 | The ca-bundle.crt file contains root Certificate Authority (CA) certificates.
5 | These are used to validate certificates presented during TLS handshake
6 | negotiation. The source for this file is the cacert.pem file available
7 | at http://curl.haxx.se/docs/caextract.html.
8 |
9 | Any self-signed CA certificate placed in this directory will be considered
10 | a trusted certificate. For example, let's say you're running the wonderland.lit
11 | XMPP server and would like to and verify the ssl cert during TLS.
12 | The wonderland.lit server hasn't purchased a legitimate TLS certificate
13 | from a CA known in ca-bundle.crt. Instead, they've created a self-signed
14 | certificate and sent it to you. Place the certificate in this directory
15 | with a name of wonderland.lit.crt and it will be trusted. Trusted TLS connections
16 | from wonderland.lit will now work.
17 |
18 | Alternatively, you can purchase a TLS certificate from a CA (e.g. Go Daddy,
19 | VeriSign, etc.) and place it in this directory. This will avoid the hassles
20 | of managing self-signed certificates.
21 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub_owner_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSubOwner do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:pubsub, 'http://jabber.org/protocol/pubsub#owner')).to eq(Blather::Stanza::PubSubOwner)
7 | end
8 |
9 | it 'ensures a pubusb node is present on create' do
10 | pubsub = Blather::Stanza::PubSubOwner.new
11 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_nil
12 | end
13 |
14 | it 'ensures a pubsub node exists when calling #pubsub' do
15 | pubsub = Blather::Stanza::PubSubOwner.new
16 | pubsub.remove_children :pubsub
17 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSubOwner.registered_ns)).to be_nil
18 |
19 | expect(pubsub.pubsub).not_to be_nil
20 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_nil
21 | end
22 |
23 | it 'sets the host if requested' do
24 | aff = Blather::Stanza::PubSubOwner.new :get, 'pubsub.jabber.local'
25 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local'))
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/blather/stanza/muc/muc_user_base.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class MUC
4 |
5 | module MUCUserBase
6 | MUC_USER_NAMESPACE = "http://jabber.org/protocol/muc#user"
7 |
8 | def self.included(klass)
9 | klass.extend ClassMethods
10 | end
11 |
12 | module ClassMethods
13 | def new(*args)
14 | super.tap { |e| e.muc_user }
15 | end
16 | end
17 |
18 | def inherit(node)
19 | muc_user.remove
20 | super
21 | self
22 | end
23 |
24 | def password
25 | find_password_node && password_node.content
26 | end
27 |
28 | def password=(var)
29 | password_node.content = var
30 | end
31 |
32 | def muc_user
33 | unless muc_user = find_first('ns:x', :ns => MUC_USER_NAMESPACE)
34 | self << (muc_user = XMPPNode.new('x', self.document))
35 | muc_user.namespace = self.class.registered_ns
36 | end
37 | muc_user
38 | end
39 |
40 | def password_node
41 | unless pw = find_password_node
42 | muc_user << (pw = XMPPNode.new('password', self.document))
43 | end
44 | pw
45 | end
46 |
47 | def find_password_node
48 | muc_user.find_first 'ns:password', :ns => MUC_USER_NAMESPACE
49 | end
50 | end # MUCUserBase
51 |
52 | end # MUC
53 | end # Stanza
54 | end # Blather
55 |
--------------------------------------------------------------------------------
/lib/blather/stanza/iq/ping.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class Iq
4 |
5 | # # Ping Stanza
6 | #
7 | # [XEP-0199: XMPP Ping](http://xmpp.org/extensions/xep-0199.html)
8 | #
9 | # This is a base class for any Ping based Iq stanzas.
10 | #
11 | # @handler :ping
12 | class Ping < Iq
13 | # @private
14 | register :ping, :ping, 'urn:xmpp:ping'
15 |
16 | # Overrides the parent method to ensure a ping node is created
17 | #
18 | # @see Blather::Stanza::Iq.new
19 | def self.new(type = :get, to = nil, id = nil)
20 | node = super
21 | node.ping
22 | node
23 | end
24 |
25 | # Overrides the parent method to ensure the current ping node is destroyed
26 | #
27 | # @see Blather::Stanza::Iq#inherit
28 | def inherit(node)
29 | ping.remove
30 | super
31 | end
32 |
33 | # Ping node accessor
34 | # If a ping node exists it will be returned.
35 | # Otherwise a new node will be created and returned
36 | #
37 | # @return [Balather::XMPPNode]
38 | def ping
39 | p = find_first 'ns:ping', :ns => self.class.registered_ns
40 |
41 | unless p
42 | (self << (p = XMPPNode.new('ping', self.document)))
43 | p.namespace = self.class.registered_ns
44 | end
45 | p
46 | end
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/blather/stanza/iq/query.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class Iq
4 |
5 | # # Query Stanza
6 | #
7 | # This is a base class for any query based Iq stanzas. It provides a base set
8 | # of methods for working with query stanzas
9 | #
10 | # @handler :query
11 | class Query < Iq
12 | register :query, :query
13 |
14 | # Overrides the parent method to ensure a query node is created
15 | #
16 | # @see Blather::Stanza::Iq.new
17 | def self.new(*)
18 | node = super
19 | node.query
20 | node
21 | end
22 |
23 | # Overrides the parent method to ensure the current query node is destroyed
24 | #
25 | # @see Blather::Stanza::Iq#inherit
26 | def inherit(node)
27 | query.remove
28 | super
29 | end
30 |
31 | # Query node accessor
32 | # If a query node exists it will be returned.
33 | # Otherwise a new node will be created and returned
34 | #
35 | # @return [Balather::XMPPNode]
36 | def query
37 | q = if self.class.registered_ns
38 | find_first('query_ns:query', :query_ns => self.class.registered_ns)
39 | else
40 | find_first('query')
41 | end
42 |
43 | unless q
44 | (self << (q = XMPPNode.new('query', self.document)))
45 | q.namespace = self.class.registered_ns
46 | end
47 | q
48 | end
49 | end #Query
50 |
51 | end #Iq
52 | end #Stanza
53 | end
54 |
--------------------------------------------------------------------------------
/spec/blather/stanza/presence/muc_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def muc_xml
4 | <<-XML
5 |
8 |
9 |
10 | XML
11 | end
12 |
13 | describe 'Blather::Stanza::Presence::MUC' do
14 | it 'registers itself' do
15 | expect(Blather::XMPPNode.class_from_registration(:x, 'http://jabber.org/protocol/muc' )).to eq(Blather::Stanza::Presence::MUC)
16 | end
17 |
18 | it 'must be importable' do
19 | c = Blather::XMPPNode.parse(muc_xml)
20 | expect(c).to be_kind_of Blather::Stanza::Presence::MUC::InstanceMethods
21 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns).count).to eq(1)
22 | end
23 |
24 | it 'ensures a form node is present on create' do
25 | c = Blather::Stanza::Presence::MUC.new
26 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns)).not_to be_empty
27 | end
28 |
29 | it 'ensures a form node exists when calling #muc' do
30 | c = Blather::Stanza::Presence::MUC.new
31 | c.remove_children :x
32 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns)).to be_empty
33 |
34 | expect(c.muc).not_to be_nil
35 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUC.registered_ns)).not_to be_empty
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/blather/stanza/iq/ping_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def ping_xml
4 | <<-XML
5 |
6 |
7 |
8 | XML
9 | end
10 |
11 | describe Blather::Stanza::Iq::Ping do
12 | it 'registers itself' do
13 | expect(Blather::XMPPNode.class_from_registration(:ping, 'urn:xmpp:ping')).to eq(Blather::Stanza::Iq::Ping)
14 | end
15 |
16 | it 'can be imported' do
17 | node = Blather::XMPPNode.parse ping_xml
18 | expect(node).to be_instance_of Blather::Stanza::Iq::Ping
19 | end
20 |
21 | it 'ensures a ping node is present on create' do
22 | iq = Blather::Stanza::Iq::Ping.new
23 | expect(iq.xpath('ns:ping', :ns => 'urn:xmpp:ping')).not_to be_empty
24 | end
25 |
26 | it 'ensures a ping node exists when calling #ping' do
27 | iq = Blather::Stanza::Iq::Ping.new
28 | iq.ping.remove
29 | expect(iq.xpath('ns:ping', :ns => 'urn:xmpp:ping')).to be_empty
30 |
31 | expect(iq.ping).not_to be_nil
32 | expect(iq.xpath('ns:ping', :ns => 'urn:xmpp:ping')).not_to be_empty
33 | end
34 |
35 | it 'responds with an empty IQ' do
36 | ping = Blather::Stanza::Iq::Ping.new :get, 'one@example.com', 'abc123'
37 | ping.from = 'two@example.com'
38 | expected_pong = Blather::Stanza::Iq::Ping.new(:result, 'two@example.com', 'abc123').tap do |pong|
39 | pong.from = 'one@example.com'
40 | end
41 | reply = ping.reply
42 | expect(reply).to eq(expected_pong)
43 | expect(reply.children.count).to eq(0)
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub_owner/purge.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSubOwner
4 |
5 | # # PubSubOwner Purge Stanza
6 | #
7 | # [XEP-0060 Section 8.5 - Purge All Node Items](http://xmpp.org/extensions/xep-0060.html#owner-purge)
8 | #
9 | # @handler :pubsub_purge
10 | class Purge < PubSubOwner
11 | register :pubsub_purge, :purge, self.registered_ns
12 |
13 | # Create a new purge stanza
14 | #
15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type
16 | # @param [String] host the host to send the request to
17 | # @param [String] node the name of the node to purge
18 | def self.new(type = :set, host = nil, node = nil)
19 | new_node = super(type, host)
20 | new_node.node = node
21 | new_node
22 | end
23 |
24 | # Get the name of the node to delete
25 | #
26 | # @return [String]
27 | def node
28 | purge_node[:node]
29 | end
30 |
31 | # Set the name of the node to delete
32 | #
33 | # @param [String] node
34 | def node=(node)
35 | purge_node[:node] = node
36 | end
37 |
38 | # Get or create the actual purge node on the stanza
39 | #
40 | # @return [Blather::XMPPNode]
41 | def purge_node
42 | unless purge_node = pubsub.find_first('ns:purge', :ns => self.class.registered_ns)
43 | self.pubsub << (purge_node = XMPPNode.new('purge', self.document))
44 | purge_node.namespace = self.pubsub.namespace
45 | end
46 | purge_node
47 | end
48 | end # Retract
49 |
50 | end # PubSub
51 | end # Stanza
52 | end # Blather
53 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub_owner/delete.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSubOwner
4 |
5 | # # PubSubOwner Delete Stanza
6 | #
7 | # [XEP-0060 Section 8.4 Delete a Node](http://xmpp.org/extensions/xep-0060.html#owner-delete)
8 | #
9 | # @handler :pubsub_delete
10 | class Delete < PubSubOwner
11 | register :pubsub_delete, :delete, self.registered_ns
12 |
13 | # Create a new delete stanza
14 | #
15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type
16 | # @param [String] host the host to send the request to
17 | # @param [String] node the name of the node to delete
18 | def self.new(type = :set, host = nil, node = nil)
19 | new_node = super(type, host)
20 | new_node.node = node
21 | new_node
22 | end
23 |
24 | # Get the name of the node to delete
25 | #
26 | # @return [String]
27 | def node
28 | delete_node[:node]
29 | end
30 |
31 | # Set the name of the node to delete
32 | #
33 | # @param [String] node
34 | def node=(node)
35 | delete_node[:node] = node
36 | end
37 |
38 | # Get or create the actual delete node on the stanza
39 | #
40 | # @return [Blather::XMPPNode]
41 | def delete_node
42 | unless delete_node = pubsub.find_first('ns:delete', :ns => self.class.registered_ns)
43 | self.pubsub << (delete_node = XMPPNode.new('delete', self.document))
44 | delete_node.namespace = self.pubsub.namespace
45 | end
46 | delete_node
47 | end
48 | end # Retract
49 |
50 | end # PubSub
51 | end # Stanza
52 | end # Blather
53 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub_owner.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 |
4 | # # PubSubOwner Base Class
5 | #
6 | # [XEP-0060 - Publish-Subscribe](http://xmpp.org/extensions/xep-0060.html)
7 | #
8 | # @handler :pubsub_owner
9 | class PubSubOwner < Iq
10 | register :pubsub_owner, :pubsub, 'http://jabber.org/protocol/pubsub#owner'
11 |
12 | # Creates the proper class from the stana's child
13 | # @private
14 | def self.import(node)
15 | klass = nil
16 | if pubsub = node.document.find_first('//ns:pubsub', :ns => self.registered_ns)
17 | pubsub.children.each { |e| break if klass = class_from_registration(e.element_name, (e.namespace.href if e.namespace)) }
18 | end
19 | (klass || self).new(node[:type]).inherit(node)
20 | end
21 |
22 | # Overrides the parent to ensure a pubsub node is created
23 | # @private
24 | def self.new(type = nil, host = nil)
25 | new_node = super type
26 | new_node.to = host
27 | new_node.pubsub
28 | new_node
29 | end
30 |
31 | # Overrides the parent to ensure the pubsub node is destroyed
32 | # @private
33 | def inherit(node)
34 | remove_children :pubsub
35 | super
36 | end
37 |
38 | # Get or create the pubsub node on the stanza
39 | #
40 | # @return [Blather::XMPPNode]
41 | def pubsub
42 | unless p = find_first('ns:pubsub', :ns => self.class.registered_ns)
43 | self << (p = XMPPNode.new('pubsub', self.document))
44 | p.namespace = self.class.registered_ns
45 | end
46 | p
47 | end
48 | end # PubSubOwner
49 |
50 | end # Stanza
51 | end # Blather
52 |
--------------------------------------------------------------------------------
/spec/blather/xmpp_node_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::XMPPNode do
4 | before { @doc = Nokogiri::XML::Document.new }
5 |
6 | it 'generates a node based on the registered_name' do
7 | foo = Class.new(Blather::XMPPNode)
8 | foo.registered_name = 'foo'
9 | expect(foo.new.element_name).to eq('foo')
10 | end
11 |
12 | it 'sets the namespace on creation' do
13 | foo = Class.new(Blather::XMPPNode)
14 | foo.registered_ns = 'foo'
15 | expect(foo.new('foo').namespace.href).to eq('foo')
16 | end
17 |
18 | it 'registers sub classes' do
19 | class RegistersSubClass < Blather::XMPPNode; register 'foo', 'foo:bar'; end
20 | expect(RegistersSubClass.registered_name).to eq('foo')
21 | expect(RegistersSubClass.registered_ns).to eq('foo:bar')
22 | expect(Blather::XMPPNode.class_from_registration('foo', 'foo:bar')).to eq(RegistersSubClass)
23 | end
24 |
25 | it 'imports another node' do
26 | class ImportSubClass < Blather::XMPPNode; register 'foo', 'foo:bar'; end
27 | n = Blather::XMPPNode.new('foo')
28 | n.namespace = 'foo:bar'
29 | expect(Blather::XMPPNode.import(n)).to be_kind_of ImportSubClass
30 | end
31 |
32 | it 'can convert itself into a stanza' do
33 | class StanzaConvert < Blather::XMPPNode; register 'foo'; end
34 | n = Blather::XMPPNode.new('foo')
35 | expect(n.to_stanza).to be_kind_of StanzaConvert
36 | end
37 |
38 | it 'can parse a string and import it' do
39 | class StanzaParse < Blather::XMPPNode; register 'foo'; end
40 | string = ''
41 | n = Nokogiri::XML(string).root
42 | i = Blather::XMPPNode.import n
43 | expect(i).to be_kind_of StanzaParse
44 | p = Blather::XMPPNode.parse string
45 | expect(p).to be_kind_of StanzaParse
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/blather/stream/features/resource.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 |
4 | # @private
5 | class Resource < Features
6 | BIND_NS = 'urn:ietf:params:xml:ns:xmpp-bind'.freeze
7 | register BIND_NS
8 |
9 | def initialize(stream, succeed, fail)
10 | super
11 | @jid = stream.jid
12 | end
13 |
14 | def receive_data(stanza)
15 | @node = stanza
16 | case stanza.element_name
17 | when 'bind' then bind
18 | when 'iq' then result
19 | else fail!(UnknownResponse.new(@node))
20 | end
21 | end
22 |
23 | private
24 | ##
25 | # Respond to the bind request
26 | # If @jid has a resource set already request it from the server
27 | def bind
28 | response = Stanza::Iq.new :set
29 | @id = response.id
30 |
31 | response << (binder = XMPPNode.new('bind', response.document))
32 | binder.namespace = BIND_NS
33 |
34 | if @jid.resource
35 | binder << (resource = XMPPNode.new('resource', binder.document))
36 | resource.content = @jid.resource
37 | end
38 |
39 | @stream.send response
40 | end
41 |
42 | ##
43 | # Process the result from the server
44 | # Sets the sends the JID (now bound to a resource)
45 | # back to the stream
46 | def result
47 | if @node[:type] == 'error'
48 | fail! StanzaError.import(@node)
49 | return
50 | end
51 |
52 | # ensure this is a response to our original request
53 | if @id == @node['id']
54 | @stream.jid = JID.new @node.find_first('bind_ns:bind/bind_ns:jid', :bind_ns => BIND_NS).content
55 | succeed!
56 | else
57 | fail!("BIND result ID mismatch. Expected: #{@id}. Received: #{@node['id']}")
58 | end
59 | end
60 | end #Resource
61 |
62 | end #Stream
63 | end #Blather
64 |
--------------------------------------------------------------------------------
/blather.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "blather/version"
4 |
5 | module RubyVersion
6 | def rbx?
7 | defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
8 | end
9 |
10 | def jruby?
11 | RUBY_PLATFORM =~ /java/
12 | end
13 | end
14 |
15 | include RubyVersion
16 | Gem::Specification.extend RubyVersion
17 |
18 | Gem::Specification.new do |s|
19 | s.name = "blather"
20 | s.version = Blather::VERSION
21 | s.platform = Gem::Platform::RUBY
22 | s.authors = ["Jeff Smick", "Ben Langfeld"]
23 | s.email = %q{blather@adhearsion.com}
24 | s.homepage = "http://adhearsion.com/blather"
25 | s.summary = %q{Simpler XMPP built for speed}
26 | s.description = %q{An XMPP DSL for Ruby written on top of EventMachine and Nokogiri}
27 | s.license = 'MIT'
28 |
29 | s.files = `git ls-files`.split("\n")
30 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
31 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
32 | s.require_paths = ["lib"]
33 |
34 | s.rdoc_options = %w{--charset=UTF-8}
35 | s.extra_rdoc_files = %w{LICENSE README.md}
36 |
37 | s.add_dependency "eventmachine", ["~> 1.2", ">= 1.2.6"]
38 | s.add_dependency "nokogiri", ["~> 1.8", ">= 1.8.3"]
39 | s.add_dependency "niceogiri", ["~> 1.0"]
40 | s.add_dependency "activesupport", [">= 2.3.11"]
41 |
42 | s.add_development_dependency "rake"
43 | s.add_development_dependency "rspec", ["~> 3.0"]
44 | s.add_development_dependency "mocha", ["~> 1.0"]
45 | s.add_development_dependency "guard-rspec"
46 | s.add_development_dependency "yard", ["~> 0.9.11"]
47 | s.add_development_dependency "bluecloth" unless jruby? || rbx?
48 | s.add_development_dependency "countdownlatch"
49 | s.add_development_dependency 'rb-fsevent', ['~> 0.9']
50 | end
51 |
--------------------------------------------------------------------------------
/lib/blather/errors.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | # Main error class
3 | # This starts the error hierarchy
4 | #
5 | # @handler :error
6 | class BlatherError < StandardError
7 | class_attribute :handler_hierarchy
8 | self.handler_hierarchy ||= []
9 |
10 | # @private
11 | @@handler_list = []
12 |
13 | # Register the class's handler
14 | #
15 | # @param [Symbol] handler the handler name
16 | def self.register(handler)
17 | @@handler_list << handler
18 | self.handler_hierarchy = [handler] + self.handler_hierarchy
19 | end
20 |
21 | # The list of registered handlers
22 | #
23 | # @return [Array] a list of currently registered handlers
24 | def self.handler_list
25 | @@handler_list
26 | end
27 |
28 | register :error
29 |
30 | # @private
31 | # HACK!! until I can refactor the entire Error object model
32 | def id
33 | nil
34 | end
35 | end # BlatherError
36 |
37 | # Used in cases where a stanza only allows specific values for its attributes
38 | # and an invalid value is attempted.
39 | #
40 | # @handler :argument_error
41 | class ArgumentError < BlatherError
42 | register :argument_error
43 | end # ArgumentError
44 |
45 | # The stream handler received a response it didn't know how to handle
46 | #
47 | # @handler :unknown_response_error
48 | class UnknownResponse < BlatherError
49 | register :unknown_response_error
50 | attr_reader :node
51 |
52 | def initialize(node)
53 | @node = node
54 | end
55 | end # UnknownResponse
56 |
57 | # Something bad happened while parsing the incoming stream
58 | #
59 | # @handler :parse_error
60 | class ParseError < BlatherError
61 | register :parse_error
62 | attr_reader :message
63 |
64 | def initialize(msg)
65 | @message = msg.to_s
66 | end
67 | end # ParseError
68 |
69 | end # Blather
70 |
--------------------------------------------------------------------------------
/spec/blather/stanza/iq/s5b_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def s5b_open_xml
4 | <<-XML
5 |
9 |
11 |
15 |
19 |
20 |
21 | XML
22 | end
23 |
24 | describe Blather::Stanza::Iq::S5b do
25 | it 'registers itself' do
26 | expect(Blather::XMPPNode.class_from_registration(:query, 'http://jabber.org/protocol/bytestreams')).to eq(Blather::Stanza::Iq::S5b)
27 | end
28 |
29 | it 'can be imported' do
30 | node = Blather::XMPPNode.parse s5b_open_xml
31 | expect(node).to be_instance_of Blather::Stanza::Iq::S5b
32 | end
33 |
34 | it 'can get sid' do
35 | node = Blather::XMPPNode.parse s5b_open_xml
36 | expect(node.sid).to eq('vxf9n471bn46')
37 | end
38 |
39 | it 'can get streamhosts' do
40 | node = Blather::XMPPNode.parse s5b_open_xml
41 | expect(node.streamhosts.size).to eq(2)
42 | end
43 |
44 | it 'can set streamhosts' do
45 | node = Blather::Stanza::Iq::S5b.new
46 | node.streamhosts += [{:jid => 'test@example.com/foo', :host => '192.168.5.1', :port => 123}]
47 | expect(node.streamhosts.size).to eq(1)
48 | node.streamhosts += [Blather::Stanza::Iq::S5b::StreamHost.new('test2@example.com/foo', '192.168.5.2', 123)]
49 | expect(node.streamhosts.size).to eq(2)
50 | end
51 |
52 | it 'can get and set streamhost-used' do
53 | node = Blather::Stanza::Iq::S5b.new
54 | node.streamhost_used = 'used@example.com/foo'
55 | expect(node.streamhost_used.jid.to_s).to eq('used@example.com/foo')
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/blather/stanza/iq/ibr.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class Iq
4 |
5 | # # In-Band Registration
6 | #
7 | # [XEP-0077: In-Band Registration](https://xmpp.org/extensions/xep-0077.html)
8 | #
9 | # @handler :ibr
10 | class IBR < Query
11 | register :ibr, nil, "jabber:iq:register"
12 |
13 | def registered=(reg)
14 | query.at_xpath("./ns:registered", ns: self.class.registered_ns)&.remove
15 | node = Nokogiri::XML::Node.new("registered", document)
16 | node.default_namespace = self.class.registered_ns
17 | query << node if reg
18 | end
19 |
20 | def registered?
21 | !!query.at_xpath("./ns:registered", ns: self.class.registered_ns)
22 | end
23 |
24 | def remove!
25 | query.children.remove
26 | node = Nokogiri::XML::Node.new("remove", document)
27 | node.default_namespace = self.class.registered_ns
28 | query << node
29 | end
30 |
31 | def remove?
32 | !!query.at_xpath("./ns:remove", ns: self.class.registered_ns)
33 | end
34 |
35 | def form
36 | X.find_or_create(query)
37 | end
38 |
39 | [
40 | "instructions",
41 | "username",
42 | "nick",
43 | "password",
44 | "name",
45 | "first",
46 | "last",
47 | "email",
48 | "address",
49 | "city",
50 | "state",
51 | "zip",
52 | "phone",
53 | "url",
54 | "date"
55 | ].each do |tag|
56 | define_method("#{tag}=") do |v|
57 | query.at_xpath("./ns:#{tag}", ns: self.class.registered_ns)&.remove
58 | node = Nokogiri::XML::Node.new(tag, document)
59 | node.default_namespace = self.class.registered_ns
60 | node.content = v
61 | query << node
62 | end
63 |
64 | define_method(tag) do
65 | query.at_xpath("./ns:#{tag}", ns: self.class.registered_ns)&.content
66 | end
67 | end
68 | end
69 |
70 | end #Iq
71 | end #Stanza
72 | end
73 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub_owner/purge_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSubOwner::Purge do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:purge, 'http://jabber.org/protocol/pubsub#owner')).to eq(Blather::Stanza::PubSubOwner::Purge)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(<<-NODE)).to be_instance_of Blather::Stanza::PubSubOwner::Purge
11 |
15 |
16 |
17 |
18 |
19 | NODE
20 | end
21 |
22 | it 'ensures an purge node is present on create' do
23 | purge = Blather::Stanza::PubSubOwner::Purge.new
24 | expect(purge.find('//ns:pubsub/ns:purge', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'ensures an purge node exists when calling #purge_node' do
28 | purge = Blather::Stanza::PubSubOwner::Purge.new
29 | purge.pubsub.remove_children :purge
30 | expect(purge.find('//ns:pubsub/ns:purge', :ns => Blather::Stanza::PubSubOwner.registered_ns)).to be_empty
31 |
32 | expect(purge.purge_node).not_to be_nil
33 | expect(purge.find('//ns:pubsub/ns:purge', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty
34 | end
35 |
36 | it 'defaults to a set node' do
37 | purge = Blather::Stanza::PubSubOwner::Purge.new
38 | expect(purge.type).to eq(:set)
39 | end
40 |
41 | it 'sets the host if requested' do
42 | purge = Blather::Stanza::PubSubOwner::Purge.new :set, 'pubsub.jabber.local'
43 | expect(purge.to).to eq(Blather::JID.new('pubsub.jabber.local'))
44 | end
45 |
46 | it 'sets the node' do
47 | purge = Blather::Stanza::PubSubOwner::Purge.new :set, 'host', 'node-name'
48 | expect(purge.node).to eq('node-name')
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/blather/stanza/iq/ibb.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class Iq
4 | # # In-Band Bytestreams Stanza
5 | #
6 | # [XEP-0047: In-Band Bytestreams](http://xmpp.org/extensions/xep-0047.html)
7 | #
8 | # @handler :ibb_open
9 | # @handler :ibb_data
10 | # @handler :ibb_close
11 | class Ibb < Iq
12 | # @private
13 | NS_IBB = 'http://jabber.org/protocol/ibb'
14 |
15 | # Overrides the parent method to remove open, close and data nodes
16 | #
17 | # @see Blather::Stanza#reply
18 | def reply
19 | reply = super
20 | reply.remove_children :open
21 | reply.remove_children :close
22 | reply.remove_children :data
23 | reply
24 | end
25 |
26 | # An Open stanza to
27 | class Open < Ibb
28 | register :ibb_open, :open, NS_IBB
29 |
30 | # Find open node
31 | #
32 | # @return [Nokogiri::XML::Element]
33 | def open
34 | find_first('ns:open', :ns => NS_IBB)
35 | end
36 |
37 | # Get the sid of the file transfer
38 | #
39 | # @return [String]
40 | def sid
41 | open['sid']
42 | end
43 |
44 | end
45 |
46 | # A Data stanza
47 | class Data < Ibb
48 | register :ibb_data, :data, NS_IBB
49 |
50 | # Find data node
51 | #
52 | # @return [Nokogiri::XML::Element]
53 | def data
54 | find_first('ns:data', :ns => NS_IBB)
55 | end
56 |
57 | # Get the sid of the file transfer
58 | #
59 | # @return [String]
60 | def sid
61 | data['sid']
62 | end
63 | end
64 |
65 | # A Close stanza
66 | class Close < Ibb
67 | register :ibb_close, :close, NS_IBB
68 |
69 | # Find close node
70 | #
71 | # @return [Nokogiri::XML::Element]
72 | def close
73 | find_first('ns:close', :ns => NS_IBB)
74 | end
75 |
76 | # Get the sid of the file transfer
77 | #
78 | # @return [String]
79 | def sid
80 | close['sid']
81 | end
82 | end
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/subscribe.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Subscribe Stanza
6 | #
7 | # [XEP-0060 Section 6.1 - Subscribe to a Node](http://xmpp.org/extensions/xep-0060.html#subscriber-subscribe)
8 | #
9 | # @handler :pubsub_subscribe
10 | class Subscribe < PubSub
11 | register :pubsub_subscribe, :subscribe, self.registered_ns
12 |
13 | # Create a new subscription node
14 | #
15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type
16 | # @param [String] host the host name to send the request to
17 | # @param [String] node the node to subscribe to
18 | # @param [Blather::JID, #to_s] jid see {#jid=}
19 | def self.new(type = :set, host = nil, node = nil, jid = nil)
20 | new_node = super(type, host)
21 | new_node.node = node
22 | new_node.jid = jid
23 | new_node
24 | end
25 |
26 | # Get the JID of the entity to subscribe
27 | #
28 | # @return [Blather::JID]
29 | def jid
30 | JID.new(subscribe[:jid])
31 | end
32 |
33 | # Set the JID of the entity to subscribe
34 | #
35 | # @param [Blather::JID, #to_s] jid
36 | def jid=(jid)
37 | subscribe[:jid] = jid
38 | end
39 |
40 | # Get the name of the node to subscribe to
41 | #
42 | # @return [String]
43 | def node
44 | subscribe[:node]
45 | end
46 |
47 | # Set the name of the node to subscribe to
48 | #
49 | # @param [String] node
50 | def node=(node)
51 | subscribe[:node] = node
52 | end
53 |
54 | # Get or create the actual subscribe node on the stanza
55 | #
56 | # @return [Blather::XMPPNode]
57 | def subscribe
58 | unless subscribe = pubsub.find_first('ns:subscribe', :ns => self.class.registered_ns)
59 | self.pubsub << (subscribe = XMPPNode.new('subscribe', self.document))
60 | subscribe.namespace = self.pubsub.namespace
61 | end
62 | subscribe
63 | end
64 | end # Subscribe
65 |
66 | end # PubSub
67 | end # Stanza
68 | end # Blather
69 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub_owner/delete_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSubOwner::Delete do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:delete, 'http://jabber.org/protocol/pubsub#owner')).to eq(Blather::Stanza::PubSubOwner::Delete)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(<<-NODE)).to be_instance_of Blather::Stanza::PubSubOwner::Delete
11 |
15 |
16 |
17 |
18 |
19 | NODE
20 | end
21 |
22 | it 'ensures an delete node is present on delete' do
23 | delete = Blather::Stanza::PubSubOwner::Delete.new
24 | expect(delete.find('//ns:pubsub/ns:delete', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'ensures an delete node exists when calling #delete_node' do
28 | delete = Blather::Stanza::PubSubOwner::Delete.new
29 | delete.pubsub.remove_children :delete
30 | expect(delete.find('//ns:pubsub/ns:delete', :ns => Blather::Stanza::PubSubOwner.registered_ns)).to be_empty
31 |
32 | expect(delete.delete_node).not_to be_nil
33 | expect(delete.find('//ns:pubsub/ns:delete', :ns => Blather::Stanza::PubSubOwner.registered_ns)).not_to be_empty
34 | end
35 |
36 | it 'defaults to a set node' do
37 | delete = Blather::Stanza::PubSubOwner::Delete.new
38 | expect(delete.type).to eq(:set)
39 | end
40 |
41 | it 'sets the host if requested' do
42 | delete = Blather::Stanza::PubSubOwner::Delete.new :set, 'pubsub.jabber.local'
43 | expect(delete.to).to eq(Blather::JID.new('pubsub.jabber.local'))
44 | end
45 |
46 | it 'sets the node' do
47 | delete = Blather::Stanza::PubSubOwner::Delete.new :set, 'host', 'node-name'
48 | expect(delete.node).to eq('node-name')
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/blather/cert_store.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 |
3 | module Blather
4 |
5 | # An X509 certificate store that validates certificate trust chains.
6 | # This uses the #{cert_directory}/*.crt files as the list of trusted root
7 | # CA certificates.
8 | class CertStore
9 | def initialize(cert_directory)
10 | @cert_directory = cert_directory
11 | @store = OpenSSL::X509::Store.new
12 | certs.each {|c| @store.add_cert(c) }
13 | end
14 |
15 | # Return true if the certificate is signed by a CA certificate in the
16 | # store. If the certificate can be trusted, it's added to the store so
17 | # it can be used to trust other certs.
18 | def trusted?(pem)
19 | if cert = OpenSSL::X509::Certificate.new(pem)
20 | @store.verify(cert).tap do |trusted|
21 | begin
22 | @store.add_cert(cert) if trusted
23 | rescue OpenSSL::X509::StoreError
24 | end
25 | end
26 | end
27 | rescue OpenSSL::X509::CertificateError
28 | nil
29 | end
30 |
31 | # Return true if the domain name matches one of the names in the
32 | # certificate. In other words, is the certificate provided to us really
33 | # for the domain to which we think we're connected?
34 | def domain?(pem, domain)
35 | if cert = OpenSSL::X509::Certificate.new(pem)
36 | OpenSSL::SSL.verify_certificate_identity(cert, domain)
37 | end
38 | end
39 |
40 | # Return the trusted root CA certificates installed in the @cert_directory. These
41 | # certificates are used to start the trust chain needed to validate certs
42 | # we receive from clients and servers.
43 | def certs
44 | @certs ||= begin
45 | pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
46 | Dir[File.join(@cert_directory, '*.crt')]
47 | .map {|f| File.read(f) }
48 | .map {|c| c.scan(pattern) }
49 | .flatten
50 | .map {|c| OpenSSL::X509::Certificate.new(c) }
51 | .reject {|c| c.not_after < Time.now }
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/create.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Create Stanza
6 | #
7 | # [XEP-0060 Section 8.1 - Create a Node](http://xmpp.org/extensions/xep-0060.html#owner-create)
8 | #
9 | # @handler :pubsub_create
10 | class Create < PubSub
11 | register :pubsub_create, :create, self.registered_ns
12 |
13 | # Create a new Create Stanza
14 | #
15 | # @param [] type the node type
16 | # @param [String, nil] host the host to send the request to
17 | # @param [String, nil] node the name of the node to create
18 | def self.new(type = :set, host = nil, node = nil)
19 | new_node = super(type, host)
20 | new_node.create_node
21 | new_node.configure_node
22 | new_node.node = node
23 | new_node
24 | end
25 |
26 | # Get the name of the node to create
27 | #
28 | # @return [String, nil]
29 | def node
30 | create_node[:node]
31 | end
32 |
33 | # Set the name of the node to create
34 | #
35 | # @param [String, nil] node
36 | def node=(node)
37 | create_node[:node] = node
38 | end
39 |
40 | # Get or create the actual create node on the stanza
41 | #
42 | # @return [Blather::XMPPNode]
43 | def create_node
44 | unless create_node = pubsub.find_first('ns:create', :ns => self.class.registered_ns)
45 | self.pubsub << (create_node = XMPPNode.new('create', self.document))
46 | create_node.namespace = self.pubsub.namespace
47 | end
48 | create_node
49 | end
50 |
51 | # Get or create the actual configure node on the stanza
52 | #
53 | # @return [Blather::XMPPNode]
54 | def configure_node
55 | unless configure_node = pubsub.find_first('ns:configure', :ns => self.class.registered_ns)
56 | self.pubsub << (configure_node = XMPPNode.new('configure', self.document))
57 | configure_node.namespace = self.pubsub.namespace
58 | end
59 | configure_node
60 | end
61 | end # Create
62 |
63 | end # PubSub
64 | end # Stanza
65 | end # Blather
66 |
--------------------------------------------------------------------------------
/spec/blather/stanza/presence/c_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def c_xml
4 | <<-XML
5 |
6 |
10 |
11 | XML
12 | end
13 |
14 | describe 'Blather::Stanza::Presence::C' do
15 | it 'registers itself' do
16 | expect(Blather::XMPPNode.class_from_registration(:c, 'http://jabber.org/protocol/caps' )).to eq(Blather::Stanza::Presence::C)
17 | end
18 |
19 | it 'must be importable' do
20 | c = Blather::XMPPNode.parse c_xml
21 | expect(c).to be_kind_of Blather::Stanza::Presence::C::InstanceMethods
22 | expect(c.hash).to eq(:'sha-1')
23 | expect(c.node).to eq('http://www.chatopus.com')
24 | expect(c.ver).to eq('zHyEOgxTrkpSdGcQKH8EFPLsriY=')
25 | end
26 |
27 | it 'ensures hash is one of Blather::Stanza::Presence::C::VALID_HASH_TYPES' do
28 | expect { Blather::Stanza::Presence::C.new nil, nil, :invalid_type_name }.to raise_error(Blather::ArgumentError)
29 |
30 | Blather::Stanza::Presence::C::VALID_HASH_TYPES.each do |valid_hash|
31 | c = Blather::Stanza::Presence::C.new nil, nil, valid_hash
32 | expect(c.hash).to eq(valid_hash.to_sym)
33 | end
34 | end
35 |
36 | it 'can set a hash on creation' do
37 | c = Blather::Stanza::Presence::C.new nil, nil, :md5
38 | expect(c.hash).to eq(:md5)
39 | end
40 |
41 | it 'can set a node on creation' do
42 | c = Blather::Stanza::Presence::C.new 'http://www.chatopus.com'
43 | expect(c.node).to eq('http://www.chatopus.com')
44 | end
45 |
46 | it 'can set a ver on creation' do
47 | c = Blather::Stanza::Presence::C.new nil, 'zHyEOgxTrkpSdGcQKH8EFPLsriY='
48 | expect(c.ver).to eq('zHyEOgxTrkpSdGcQKH8EFPLsriY=')
49 | end
50 |
51 | it 'is equal on import and creation' do
52 | p = Blather::XMPPNode.parse c_xml
53 | c = Blather::Stanza::Presence::C.new 'http://www.chatopus.com', 'zHyEOgxTrkpSdGcQKH8EFPLsriY=', 'sha-1'
54 | expect(p).to eq(c)
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/create_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub::Create do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:create, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Create)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(<<-NODE)).to be_instance_of Blather::Stanza::PubSub::Create
11 |
15 |
16 |
17 |
18 |
19 |
20 | NODE
21 | end
22 |
23 | it 'ensures a create node is present on create' do
24 | create = Blather::Stanza::PubSub::Create.new
25 | expect(create.find('//ns:pubsub/ns:create', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
26 | end
27 |
28 | it 'ensures a configure node is present on create' do
29 | create = Blather::Stanza::PubSub::Create.new
30 | expect(create.find('//ns:pubsub/ns:configure', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
31 | end
32 |
33 | it 'ensures a create node exists when calling #create_node' do
34 | create = Blather::Stanza::PubSub::Create.new
35 | create.pubsub.remove_children :create
36 | expect(create.find('//ns:pubsub/ns:create', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
37 |
38 | expect(create.create_node).not_to be_nil
39 | expect(create.find('//ns:pubsub/ns:create', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
40 | end
41 |
42 | it 'defaults to a set node' do
43 | create = Blather::Stanza::PubSub::Create.new
44 | expect(create.type).to eq(:set)
45 | end
46 |
47 | it 'sets the host if requested' do
48 | create = Blather::Stanza::PubSub::Create.new :set, 'pubsub.jabber.local'
49 | expect(create.to).to eq(Blather::JID.new('pubsub.jabber.local'))
50 | end
51 |
52 | it 'sets the node' do
53 | create = Blather::Stanza::PubSub::Create.new :set, 'host', 'node-name'
54 | expect(create.node).to eq('node-name')
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/blather/stanza/iq/query_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::Stanza::Iq::Query do
4 | it 'registers itself' do
5 | expect(Blather::XMPPNode.class_from_registration(:query, nil)).to eq(Blather::Stanza::Iq::Query)
6 | end
7 |
8 | it 'can be imported' do
9 | string = <<-XML
10 |
11 |
12 |
13 |
14 |
15 | XML
16 | expect(Blather::XMPPNode.parse(string)).to be_instance_of Blather::Stanza::Iq::Query
17 | end
18 |
19 | it 'ensures a query node is present on create' do
20 | query = Blather::Stanza::Iq::Query.new
21 | expect(query.xpath('query')).not_to be_empty
22 | end
23 |
24 | it 'ensures a query node exists when calling #query' do
25 | query = Blather::Stanza::Iq::Query.new
26 | query.remove_child :query
27 | expect(query.xpath('query')).to be_empty
28 |
29 | expect(query.query).not_to be_nil
30 | expect(query.xpath('query')).not_to be_empty
31 | end
32 |
33 | [:get, :set, :result, :error].each do |type|
34 | it "can be set as \"#{type}\"" do
35 | query = Blather::Stanza::Iq::Query.new type
36 | expect(query.type).to eq(type)
37 | end
38 | end
39 |
40 | it 'sets type to "result" on reply' do
41 | query = Blather::Stanza::Iq::Query.new
42 | expect(query.type).to eq(:get)
43 | reply = expect(query.reply.type).to eq(:result)
44 | end
45 |
46 | it 'sets type to "result" on reply!' do
47 | query = Blather::Stanza::Iq::Query.new
48 | expect(query.type).to eq(:get)
49 | query.reply!
50 | expect(query.type).to eq(:result)
51 | end
52 |
53 | it 'can be registered under a namespace' do
54 | class QueryNs < Blather::Stanza::Iq::Query; register :query_ns, nil, 'query:ns'; end
55 | expect(Blather::XMPPNode.class_from_registration(:query, 'query:ns')).to eq(QueryNs)
56 | query_ns = QueryNs.new
57 | expect(query_ns.xpath('query')).to be_empty
58 | expect(query_ns.xpath('ns:query', :ns => 'query:ns').size).to eq(1)
59 |
60 | query_ns.query
61 | query_ns.query
62 | expect(query_ns.xpath('ns:query', :ns => 'query:ns').size).to eq(1)
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/affiliations.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Affiliations Stanza
6 | #
7 | # [XEP-0060 Section 8.9 - Manage Affiliations](http://xmpp.org/extensions/xep-0060.html#owner-affiliations)
8 | #
9 | # @handler :pubsub_affiliations
10 | class Affiliations < PubSub
11 | register :pubsub_affiliations, :affiliations, self.registered_ns
12 |
13 | include Enumerable
14 | alias_method :find, :xpath
15 |
16 | # Overrides the parent to ensure an affiliation node is created
17 | # @private
18 | def self.new(type = nil, host = nil)
19 | new_node = super
20 | new_node.affiliations
21 | new_node
22 | end
23 |
24 | # Kill the affiliations node before running inherit
25 | # @private
26 | def inherit(node)
27 | affiliations.remove
28 | super
29 | end
30 |
31 | # Get or create the affiliations node
32 | #
33 | # @return [Blather::XMPPNode]
34 | def affiliations
35 | aff = pubsub.find_first('ns:affiliations', :ns => self.class.registered_ns)
36 | unless aff
37 | self.pubsub << (aff = XMPPNode.new('affiliations', self.document))
38 | end
39 | aff
40 | end
41 |
42 | # Convenience method for iterating over the list
43 | #
44 | # @see #list for the format of the yielded input
45 | def each(&block)
46 | list.each &block
47 | end
48 |
49 | # Get the number of affiliations
50 | #
51 | # @return [Fixnum]
52 | def size
53 | list.size
54 | end
55 |
56 | # Get the hash of affilations as affiliation-type => [nodes]
57 | #
58 | # @example
59 | #
60 | # { :owner => ['node1', 'node2'],
61 | # :publisher => ['node3'],
62 | # :outcast => ['node4'],
63 | # :member => ['node5'],
64 | # :none => ['node6'] }
65 | #
66 | # @return [Hash Array>]
67 | def list
68 | items = affiliations.find('//ns:affiliation', :ns => self.class.registered_ns)
69 | items.inject({}) do |hash, item|
70 | hash[item[:affiliation].to_sym] ||= []
71 | hash[item[:affiliation].to_sym] << item[:node]
72 | hash
73 | end
74 | end
75 | end # Affiliations
76 |
77 | end # PubSub
78 | end # Stanza
79 | end # Blather
80 |
--------------------------------------------------------------------------------
/spec/blather/stanza/iq_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::Stanza::Iq do
4 | it 'registers itself' do
5 | expect(Blather::XMPPNode.class_from_registration(:iq, nil)).to eq(Blather::Stanza::Iq)
6 | end
7 |
8 | it 'must be importable' do
9 | string = ""
10 | expect(Blather::XMPPNode.parse(string)).to be_instance_of Blather::Stanza::Iq
11 | end
12 |
13 | it 'creates a new Iq stanza defaulted as a get' do
14 | expect(Blather::Stanza::Iq.new.type).to eq(:get)
15 | end
16 |
17 | it 'sets the id when created' do
18 | expect(Blather::Stanza::Iq.new.id).not_to be_nil
19 | end
20 |
21 | it 'creates a new Stanza::Iq object on import' do
22 | expect(Blather::Stanza::Iq.import(Blather::XMPPNode.new('iq'))).to be_kind_of Blather::Stanza::Iq
23 | end
24 |
25 | it 'creates a proper object based on its children' do
26 | n = Blather::XMPPNode.new('iq')
27 | n << Blather::XMPPNode.new('query', n.document)
28 | expect(Blather::Stanza::Iq.import(n)).to be_kind_of Blather::Stanza::Iq::Query
29 | end
30 |
31 | it 'ensures type is one of Stanza::Iq::VALID_TYPES' do
32 | expect { Blather::Stanza::Iq.new :invalid_type_name }.to raise_error(Blather::ArgumentError)
33 |
34 | Blather::Stanza::Iq::VALID_TYPES.each do |valid_type|
35 | n = Blather::Stanza::Iq.new valid_type
36 | expect(n.type).to eq(valid_type)
37 | end
38 | end
39 |
40 | Blather::Stanza::Iq::VALID_TYPES.each do |valid_type|
41 | it "provides a helper (#{valid_type}?) for type #{valid_type}" do
42 | expect(Blather::Stanza::Iq.new).to respond_to :"#{valid_type}?"
43 | end
44 | end
45 |
46 | it 'removes the body when replying' do
47 | iq = Blather::Stanza::Iq.new :get, 'me@example.com'
48 | iq.from = 'them@example.com'
49 | iq << Blather::XMPPNode.new('query', iq.document)
50 | r = iq.reply
51 | expect(r.children.empty?).to eq(true)
52 | end
53 |
54 | it 'does not remove the body when replying if we ask to keep it' do
55 | iq = Blather::Stanza::Iq.new :get, 'me@example.com'
56 | iq.from = 'them@example.com'
57 | iq << Blather::XMPPNode.new('query', iq.document)
58 | r = iq.reply :remove_children => false
59 | expect(r.children.empty?).to eq(false)
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/affiliations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | def control_affiliations
5 | { :owner => ['node1', 'node2'],
6 | :publisher => ['node3'],
7 | :outcast => ['node4'],
8 | :member => ['node5'],
9 | :none => ['node6'] }
10 | end
11 |
12 | describe Blather::Stanza::PubSub::Affiliations do
13 | it 'registers itself' do
14 | expect(Blather::XMPPNode.class_from_registration(:affiliations, Blather::Stanza::PubSub.registered_ns)).to eq(Blather::Stanza::PubSub::Affiliations)
15 | end
16 |
17 | it 'can be imported' do
18 | expect(Blather::XMPPNode.parse(affiliations_xml)).to be_instance_of Blather::Stanza::PubSub::Affiliations
19 | end
20 |
21 | it 'ensures an affiliations node is present on create' do
22 | affiliations = Blather::Stanza::PubSub::Affiliations.new
23 | expect(affiliations.find_first('//ns:affiliations', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil
24 | end
25 |
26 | it 'ensures an affiliations node exists when calling #affiliations' do
27 | affiliations = Blather::Stanza::PubSub::Affiliations.new
28 | affiliations.pubsub.remove_children :affiliations
29 | expect(affiliations.find_first('//ns:affiliations', :ns => Blather::Stanza::PubSub.registered_ns)).to be_nil
30 |
31 | expect(affiliations.affiliations).not_to be_nil
32 | expect(affiliations.find_first('//ns:affiliations', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil
33 | end
34 |
35 | it 'defaults to a get node' do
36 | expect(Blather::Stanza::PubSub::Affiliations.new.type).to eq(:get)
37 | end
38 |
39 | it 'sets the host if requested' do
40 | aff = Blather::Stanza::PubSub::Affiliations.new :get, 'pubsub.jabber.local'
41 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local'))
42 | end
43 |
44 | it 'can import an affiliates result node' do
45 | node = parse_stanza(affiliations_xml).root
46 |
47 | affiliations = Blather::Stanza::PubSub::Affiliations.new.inherit node
48 | expect(affiliations.size).to eq(5)
49 | expect(affiliations.list).to eq(control_affiliations)
50 | end
51 |
52 | it 'will iterate over each affiliation' do
53 | Blather::XMPPNode.parse(affiliations_xml).each do |type, nodes|
54 | expect(nodes).to eq(control_affiliations[type])
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/blather/stream/features.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 |
4 | # @private
5 | class Features
6 | @@features = {}
7 | def self.register(ns)
8 | @@features[ns] = self
9 | end
10 |
11 | def self.from_namespace(ns)
12 | @@features[ns]
13 | end
14 |
15 | def initialize(stream, succeed, fail)
16 | @stream, @succeed, @fail = stream, succeed, fail
17 | end
18 |
19 | def receive_data(stanza)
20 | if @feature
21 | @feature.receive_data stanza
22 | else
23 | @features ||= stanza
24 | next!
25 | end
26 | end
27 |
28 | def next!
29 | if starttls = @features.at_xpath("tls:starttls",{"tls" => "urn:ietf:params:xml:ns:xmpp-tls"})
30 | @feature = TLS.new(@stream, nil, @fail)
31 | @feature.receive_data(starttls)
32 | return
33 | end
34 |
35 | bind = @features.at_xpath('ns:bind', ns: 'urn:ietf:params:xml:ns:xmpp-bind')
36 | session = @features.at_xpath('ns:session', ns: 'urn:ietf:params:xml:ns:xmpp-session')
37 | if bind && session && @features.children.last != session
38 | bind.after session
39 | end
40 |
41 | @idx = @idx ? @idx+1 : 0
42 | if stanza = @features.children[@idx]
43 | if stanza.namespaces['xmlns'] && (klass = self.class.from_namespace(stanza.namespaces['xmlns']))
44 | @feature = klass.new(
45 | @stream,
46 | proc {
47 | if (klass == Blather::Stream::Register && stanza = feature?(:mechanisms))
48 | @idx = @features.children.index(stanza)
49 | @feature = Blather::Stream::SASL.new @stream, proc { next! }, @fail
50 | @feature.receive_data stanza
51 | else
52 | next!
53 | end
54 | },
55 | (klass == Blather::Stream::SASL && feature?(:register)) ? proc { next! } : @fail
56 | )
57 | @feature.receive_data stanza
58 | else
59 | next!
60 | end
61 | else
62 | succeed!
63 | end
64 | end
65 |
66 | def succeed!
67 | @succeed.call
68 | end
69 |
70 | def fail!(msg)
71 | @fail.call msg
72 | end
73 |
74 | def feature?(feature)
75 | @features && @features.children.find { |v| v.element_name == feature.to_s }
76 | end
77 | end
78 |
79 | end #Stream
80 | end #Blather
81 |
--------------------------------------------------------------------------------
/examples/print_hierarchy.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'blather'
3 |
4 | class Object
5 | begin
6 | ObjectSpace.each_object(Class.new) {}
7 |
8 | # Exclude this class unless it's a subclass of our supers and is defined.
9 | # We check defined? in case we find a removed class that has yet to be
10 | # garbage collected. This also fails for anonymous classes -- please
11 | # submit a patch if you have a workaround.
12 | def subclasses_of(*superclasses)
13 | subclasses = []
14 |
15 | superclasses.each do |sup|
16 | ObjectSpace.each_object(class << sup; self; end) do |k|
17 | if k != sup && (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id"))
18 | subclasses << k
19 | end
20 | end
21 | end
22 |
23 | subclasses
24 | end
25 | rescue RuntimeError
26 | # JRuby and any implementations which cannot handle the objectspace traversal
27 | # above fall back to this implementation
28 | def subclasses_of(*superclasses)
29 | subclasses = []
30 |
31 | superclasses.each do |sup|
32 | ObjectSpace.each_object(Class) do |k|
33 | if superclasses.any? { |superclass| k < superclass } &&
34 | (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id"))
35 | subclasses << k
36 | end
37 | end
38 | subclasses.uniq!
39 | end
40 | subclasses
41 | end
42 | end
43 | end
44 |
45 | class Hash
46 | def deep_merge(second)
47 | # From: http://www.ruby-forum.com/topic/142809
48 | # Author: Stefan Rusterholz
49 | merger = proc { |key,v1,v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
50 | self.merge(second, &merger)
51 | end
52 | end
53 |
54 | handlers = {}
55 | (Object.subclasses_of(Blather::Stanza) + Object.subclasses_of(Blather::BlatherError)).each do |klass|
56 | handlers = handlers.deep_merge klass.handler_hierarchy.inject('klass' => klass.to_s.gsub('Blather::', '')) { |h,k| {k.to_s => h} }
57 | end
58 |
59 | level = 0
60 | runner = proc do |k,v|
61 | next if k == 'klass'
62 |
63 | str = ''
64 | if level > 0
65 | (level - 1).times { str << '| ' }
66 | str << '|- '
67 | end
68 |
69 | puts str+k
70 | if Hash === v
71 | level += 1
72 | v.sort.each &runner
73 | level -= 1
74 | end
75 | end
76 |
77 | handlers.sort.each &runner
78 |
--------------------------------------------------------------------------------
/lib/blather/errors/stream_error.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 |
3 | # Stream Errors
4 | # [RFC3920 Section 9.3](http://xmpp.org/rfcs/rfc3920.html#streams-error-rules)
5 | #
6 | # @handler :stream_error
7 | class StreamError < BlatherError
8 | # @private
9 | STREAM_ERR_NS = 'urn:ietf:params:xml:ns:xmpp-streams'
10 |
11 | register :stream_error
12 |
13 | attr_reader :text, :extras
14 |
15 | # Factory method for instantiating the proper class for the error
16 | #
17 | # @param [Blather::XMPPNode] node the importable node
18 | def self.import(node)
19 | name = node.find_first('descendant::*[name()!="text"]', STREAM_ERR_NS).element_name
20 |
21 | text = node.find_first 'descendant::*[name()="text"]', STREAM_ERR_NS
22 | text = text.content if text
23 |
24 | extras = node.find("descendant::*[namespace-uri()!='#{STREAM_ERR_NS}']").map { |n| n }
25 |
26 | self.new name, text, extras
27 | end
28 |
29 | # Create a new Stream Error
30 | # [RFC3920 Section 4.7.2](http://xmpp.org/rfcs/rfc3920.html#rfc.section.4.7.2)
31 | #
32 | # @param [String] name the error name
33 | # @param [String, nil] text optional error text
34 | # @param [Array] extras an array of extras to attach to the
35 | # error
36 | def initialize(name, text = nil, extras = [])
37 | @name = name
38 | @text = text
39 | @extras = extras
40 | end
41 |
42 | # The error name
43 | #
44 | # @return [Symbol]
45 | def name
46 | @name.gsub('-','_').to_sym
47 | end
48 |
49 | # Creates an XML node from the error
50 | #
51 | # @return [Blather::XMPPNode]
52 | def to_node
53 | node = XMPPNode.new('error')
54 | node.namespace = {'stream' => Blather::Stream::STREAM_NS}
55 |
56 | node << (err = XMPPNode.new(@name, node.document))
57 | err.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
58 |
59 | if self.text
60 | node << (text = XMPPNode.new('text', node.document))
61 | text.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
62 | text.content = self.text
63 | end
64 |
65 | self.extras.each { |extra| node << extra.dup }
66 | node
67 | end
68 |
69 | # Convert the object to a proper node then convert it to a string
70 | #
71 | # @return [String]
72 | def to_xml(*args)
73 | to_node.to_xml(*args)
74 | end
75 |
76 | # @private
77 | def inspect
78 | "Stream Error (#{@name}): #{self.text}" + (self.extras.empty? ? '' : " [#{self.extras}]")
79 | end
80 | # @private
81 | alias_method :to_s, :inspect
82 | end # StreamError
83 |
84 | end # Blather
85 |
--------------------------------------------------------------------------------
/lib/blather/file_transfer/ibb.rb:
--------------------------------------------------------------------------------
1 | require "base64"
2 |
3 | module Blather
4 | class FileTransfer
5 | # In-Band Bytestreams Transfer helper
6 | # Takes care of accepting, declining and offering file transfers through the stream
7 | class Ibb
8 | def initialize(stream, iq)
9 | @stream = stream
10 | @iq = iq
11 | @seq = 0
12 | end
13 |
14 | # Accept an incoming file-transfer
15 | #
16 | # @param [module] handler the handler for incoming data, see Blather::FileTransfer::SimpleFileReceiver for an example
17 | # @param [Array] params the params to be passed into the handler
18 | def accept(handler, *params)
19 | @io_read, @io_write = IO.pipe
20 | EM::attach @io_read, handler, *params
21 |
22 | @stream.register_handler :ibb_data, :from => @iq.from, :sid => @iq.sid do |iq|
23 | if iq.data['seq'] == @seq.to_s
24 | begin
25 | @io_write << Base64.decode64(iq.data.content)
26 |
27 | @stream.write iq.reply
28 |
29 | @seq += 1
30 | @seq = 0 if @seq > 65535
31 | rescue Errno::EPIPE => e
32 | @stream.write StanzaError.new(iq, 'not-acceptable', :cancel).to_node
33 | end
34 | else
35 | @stream.write StanzaError.new(iq, 'unexpected-request', :wait).to_node
36 | end
37 | true
38 | end
39 |
40 | @stream.register_handler :ibb_close, :from => @iq.from, :sid => @iq.sid do |iq|
41 | @stream.write iq.reply
42 | @stream.clear_handlers :ibb_data, :from => @iq.from, :sid => @iq.sid
43 | @stream.clear_handlers :ibb_close, :from => @iq.from, :sid => @iq.sid
44 |
45 | @io_write.close
46 | true
47 | end
48 |
49 | @stream.clear_handlers :ibb_open, :from => @iq.from
50 | @stream.clear_handlers :ibb_open, :from => @iq.from, :sid => @iq.sid
51 | @stream.write @iq.reply
52 | end
53 |
54 | # Decline an incoming file-transfer
55 | def decline
56 | @stream.clear_handlers :ibb_open, :from => @iq.from
57 | @stream.clear_handlers :ibb_data, :from => @iq.from, :sid => @iq.sid
58 | @stream.clear_handlers :ibb_close, :from => @iq.from, :sid => @iq.sid
59 | @stream.write StanzaError.new(@iq, 'not-acceptable', :cancel).to_node
60 | end
61 |
62 | # Offer a file to somebody, not implemented yet
63 | def offer
64 | # TODO: implement
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/unsubscribe.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Unsubscribe Stanza
6 | #
7 | # [XEP-0060 Section 6.2 - Unsubscribe from a Node](http://xmpp.org/extensions/xep-0060.html#subscriber-unsubscribe)
8 | #
9 | # @handler :pubsub_unsubscribe
10 | class Unsubscribe < PubSub
11 | register :pubsub_unsubscribe, :unsubscribe, self.registered_ns
12 |
13 | # Create a new unsubscribe node
14 | #
15 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type
16 | # @param [String] host the host to send the request to
17 | # @param [String] node the node to unsubscribe from
18 | # @param [Blather::JID, #to_s] jid the JID of the unsubscription
19 | # @param [String] subid the subscription ID of the unsubscription
20 | def self.new(type = :set, host = nil, node = nil, jid = nil, subid = nil)
21 | new_node = super(type, host)
22 | new_node.node = node
23 | new_node.jid = jid
24 | new_node.subid = subid
25 | new_node
26 | end
27 |
28 | # Get the JID of the unsubscription
29 | #
30 | # @return [Blather::JID]
31 | def jid
32 | JID.new(unsubscribe[:jid])
33 | end
34 |
35 | # Set the JID of the unsubscription
36 | #
37 | # @param [Blather::JID, #to_s] jid
38 | def jid=(jid)
39 | unsubscribe[:jid] = jid
40 | end
41 |
42 | # Get the name of the node to unsubscribe from
43 | #
44 | # @return [String]
45 | def node
46 | unsubscribe[:node]
47 | end
48 |
49 | # Set the name of the node to unsubscribe from
50 | #
51 | # @param [String] node
52 | def node=(node)
53 | unsubscribe[:node] = node
54 | end
55 |
56 | # Get the subscription ID to unsubscribe from
57 | #
58 | # @return [String]
59 | def subid
60 | unsubscribe[:subid]
61 | end
62 |
63 | # Set the subscription ID to unsubscribe from
64 | #
65 | # @param [String] node
66 | def subid=(subid)
67 | unsubscribe[:subid] = subid
68 | end
69 |
70 | # Get or create the actual unsubscribe node
71 | #
72 | # @return [Blather::XMPPNode]
73 | def unsubscribe
74 | unless unsubscribe = pubsub.find_first('ns:unsubscribe', :ns => self.class.registered_ns)
75 | self.pubsub << (unsubscribe = XMPPNode.new('unsubscribe', self.document))
76 | unsubscribe.namespace = self.pubsub.namespace
77 | end
78 | unsubscribe
79 | end
80 | end # Unsubscribe
81 |
82 | end # PubSub
83 | end # Stanza
84 | end # Blather
85 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:pubsub, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub)
7 | end
8 |
9 | it 'ensures a pubusb node is present on create' do
10 | pubsub = Blather::Stanza::PubSub.new
11 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil
12 | end
13 |
14 | it 'ensures a pubsub node exists when calling #pubsub' do
15 | pubsub = Blather::Stanza::PubSub.new
16 | pubsub.remove_children :pubsub
17 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSub.registered_ns)).to be_nil
18 |
19 | expect(pubsub.pubsub).not_to be_nil
20 | expect(pubsub.find_first('/iq/ns:pubsub', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_nil
21 | end
22 |
23 | it 'sets the host if requested' do
24 | aff = Blather::Stanza::PubSub.new :get, 'pubsub.jabber.local'
25 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local'))
26 | end
27 |
28 | it 'ensures newly inherited items are PubSubItem objects' do
29 | pubsub = Blather::XMPPNode.parse(items_all_nodes_xml)
30 | expect(pubsub.items.map { |i| i.class }.uniq).to eq([Blather::Stanza::PubSub::PubSubItem])
31 | end
32 | end
33 |
34 | describe Blather::Stanza::PubSub::PubSubItem do
35 | it 'can be initialized with just an ID' do
36 | id = 'foobarbaz'
37 | item = Blather::Stanza::PubSub::Items::PubSubItem.new id
38 | expect(item.id).to eq(id)
39 | end
40 |
41 | it 'can be initialized with a payload' do
42 | payload = 'foobarbaz'
43 | item = Blather::Stanza::PubSub::Items::PubSubItem.new 'foo', payload
44 | expect(item.payload).to eq(payload)
45 | end
46 |
47 | it 'allows the payload to be set' do
48 | item = Blather::Stanza::PubSub::Items::PubSubItem.new
49 | expect(item.payload).to be_nil
50 | item.payload = 'testing'
51 | expect(item.payload).to eq('testing')
52 | expect(item.content).to eq('testing')
53 | end
54 |
55 | it 'allows the payload to be unset' do
56 | payload = 'foobarbaz'
57 | item = Blather::Stanza::PubSub::Items::PubSubItem.new 'foo', payload
58 | expect(item.payload).to eq(payload)
59 | item.payload = nil
60 | expect(item.payload).to be_nil
61 | end
62 |
63 | it 'makes payloads readable as string' do
64 | payload = Blather::XMPPNode.new 'foo'
65 | item = Blather::Stanza::PubSub::Items::PubSubItem.new 'bar', payload
66 | expect(item.payload).to eq(payload.to_s)
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/subscriptions.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Subscriptions Stanza
6 | #
7 | # [XEP-0060 Section 5.6 Retrieve Subscriptions](http://xmpp.org/extensions/xep-0060.html#entity-subscriptions)
8 | #
9 | # @handler :pubsub_subscriptions
10 | class Subscriptions < PubSub
11 | register :pubsub_subscriptions, :subscriptions, self.registered_ns
12 |
13 | include Enumerable
14 | alias_method :find, :xpath
15 |
16 | # Overrides the parent to ensure a subscriptions node is created
17 | # @private
18 | def self.new(type = nil, host = nil)
19 | new_node = super type
20 | new_node.to = host
21 | new_node.subscriptions
22 | new_node
23 | end
24 |
25 | # Overrides the parent to ensure the subscriptions node is destroyed
26 | # @private
27 | def inherit(node)
28 | subscriptions.remove
29 | super
30 | end
31 |
32 | # Get or create the actual subscriptions node
33 | #
34 | # @return [Blather::XMPPNode]
35 | def subscriptions
36 | subs = pubsub.find_first('ns:subscriptions', :ns => self.class.registered_ns)
37 | unless subs
38 | self.pubsub << (subs = XMPPNode.new('subscriptions', self.document))
39 | end
40 | subs
41 | end
42 |
43 | # Iterate over the list of subscriptions
44 | #
45 | # @yieldparam [Hash] subscription
46 | # @see {#list}
47 | def each(&block)
48 | list.each &block
49 | end
50 |
51 | # Get the size of the subscriptions list
52 | #
53 | # @return [Fixnum]
54 | def size
55 | list.size
56 | end
57 |
58 | # Get a hash of subscriptions
59 | #
60 | # @example
61 | # { :subscribed => [{:node => 'node1', :jid => 'francisco@denmark.lit', :subid => 'fd8237yr872h3f289j2'}, {:node => 'node2', :jid => 'francisco@denmark.lit', :subid => 'h8394hf8923ju'}],
62 | # :unconfigured => [{:node => 'node3', :jid => 'francisco@denmark.lit'}],
63 | # :pending => [{:node => 'node4', :jid => 'francisco@denmark.lit'}],
64 | # :none => [{:node => 'node5', :jid => 'francisco@denmark.lit'}] }
65 | #
66 | # @return [Hash]
67 | def list
68 | subscriptions.find('//ns:subscription', :ns => self.class.registered_ns).inject({}) do |hash, item|
69 | hash[item[:subscription].to_sym] ||= []
70 | sub = {
71 | :node => item[:node],
72 | :jid => item[:jid]
73 | }
74 | sub[:subid] = item[:subid] if item[:subid]
75 | hash[item[:subscription].to_sym] << sub
76 | hash
77 | end
78 | end
79 | end # Subscriptions
80 |
81 | end # PubSub
82 | end # Stanza
83 | end # Blather
84 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/retract.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Retract Stanza
6 | #
7 | # [XEP-0060 Section 7.2 - Delete an Item from a Node](http://xmpp.org/extensions/xep-0060.html#publisher-delete)
8 | #
9 | # @handler :pubsub_retract
10 | class Retract < PubSub
11 | register :pubsub_retract, :retract, self.registered_ns
12 |
13 | include Enumerable
14 | alias_method :find, :xpath
15 |
16 | # Createa new Retraction stanza
17 | #
18 | # @param [String] host the host to send the request to
19 | # @param [String] node the node to retract items from
20 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type
21 | # @param [Array] retractions an array of ids to retract
22 | def self.new(host = nil, node = nil, type = :set, retractions = [])
23 | new_node = super(type, host)
24 | new_node.node = node
25 | new_node.retractions = retractions
26 | new_node
27 | end
28 |
29 | # Get the name of the node to retract from
30 | #
31 | # @return [String]
32 | def node
33 | retract[:node]
34 | end
35 |
36 | # Set the name of the node to retract from
37 | #
38 | # @param [String] node
39 | def node=(node)
40 | retract[:node] = node
41 | end
42 |
43 | # Get or create the actual retract node
44 | #
45 | # @return [Blather::XMPPNode]
46 | def retract
47 | unless retract = pubsub.find_first('ns:retract', :ns => self.class.registered_ns)
48 | self.pubsub << (retract = XMPPNode.new('retract', self.document))
49 | retract.namespace = self.pubsub.namespace
50 | end
51 | retract
52 | end
53 |
54 | # Set the retraction ids
55 | #
56 | # @overload retractions=(id)
57 | # @param [String] id an ID to retract
58 | # @overload retractions=(ids)
59 | # @param [Array] ids an array of IDs to retract
60 | def retractions=(retractions = [])
61 | [retractions].flatten.each do |id|
62 | self.retract << PubSubItem.new(id, nil, self.document)
63 | end
64 | end
65 |
66 | # Get the list of item IDs to retract
67 | #
68 | # @return [Array]
69 | def retractions
70 | retract.find('ns:item', :ns => self.class.registered_ns).map do |i|
71 | i[:id]
72 | end
73 | end
74 |
75 | # Iterate over each retraction ID
76 | #
77 | # @yieldparam [String] id an ID to retract
78 | def each(&block)
79 | retractions.each &block
80 | end
81 |
82 | # The size of the retractions array
83 | #
84 | # @return [Fixnum]
85 | def size
86 | retractions.size
87 | end
88 | end # Retract
89 |
90 | end # PubSub
91 | end # Stanza
92 | end # Blather
93 |
--------------------------------------------------------------------------------
/lib/blather/stanza/presence/c.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class Presence
4 |
5 | # # Entity Capabilities Stanza
6 | #
7 | # [XEP-0115 - Entity Capabilities](http://http://xmpp.org/extensions/xep-0115.html)
8 | #
9 | # Blather handles c nodes through this class. It provides a set of helper methods
10 | # to quickly deal with capabilites presence stanzas.
11 | #
12 | # @handler :c
13 | class C < Presence
14 | register :c, :c, 'http://jabber.org/protocol/caps'
15 |
16 | # @private
17 | VALID_HASH_TYPES = %w[md2 md5 sha-1 sha-224 sha-256 sha-384 sha-512].freeze
18 |
19 | def self.new(node = nil, ver = nil, hash = 'sha-1')
20 | new_node = super()
21 | new_node.c
22 | new_node.hash = hash
23 | new_node.node = node
24 | new_node.ver = ver
25 | parse new_node.to_xml
26 | end
27 |
28 | module InstanceMethods
29 |
30 | # @private
31 | def inherit(node)
32 | c.remove
33 | super
34 | self
35 | end
36 |
37 | # Get the name of the node
38 | #
39 | # @return [String, nil]
40 | def node
41 | c[:node]
42 | end
43 |
44 | # Set the name of the node
45 | #
46 | # @param [String, nil] node the new node name
47 | def node=(node)
48 | c[:node] = node
49 | end
50 |
51 | # Get the name of the hash
52 | #
53 | # @return [Symbol, nil]
54 | def hash
55 | c[:hash] && c[:hash].to_sym
56 | end
57 |
58 | # Set the name of the hash
59 | #
60 | # @param [String, nil] hash the new hash name
61 | def hash=(hash)
62 | if hash && !VALID_HASH_TYPES.include?(hash.to_s)
63 | raise ArgumentError, "Invalid Hash Type (#{hash}), use: #{VALID_HASH_TYPES*' '}"
64 | end
65 | c[:hash] = hash
66 | end
67 |
68 | # Get the ver
69 | #
70 | # @return [String, nil]
71 | def ver
72 | c[:ver]
73 | end
74 |
75 | # Set the ver
76 | #
77 | # @param [String, nil] ver the new ver
78 | def ver=(ver)
79 | c[:ver] = ver
80 | end
81 |
82 | # C node accessor
83 | # If a c node exists it will be returned.
84 | # Otherwise a new node will be created and returned
85 | #
86 | # @return [Blather::XMPPNode]
87 | def c
88 | unless c = find_first('ns:c', :ns => C.registered_ns)
89 | self << (c = XMPPNode.new('c', self.document))
90 | c.namespace = self.class.registered_ns
91 | end
92 | c
93 | end
94 | end
95 |
96 | include InstanceMethods
97 | end # C
98 | end #Presence
99 | end #Stanza
100 | end
101 |
--------------------------------------------------------------------------------
/lib/blather/client.rb:
--------------------------------------------------------------------------------
1 | require 'optparse'
2 |
3 | if !defined?(Blather::DSL)
4 | require File.join(File.dirname(__FILE__), *%w[client dsl])
5 |
6 | include Blather::DSL
7 | end
8 |
9 | options = {}
10 | optparse = OptionParser.new do |opts|
11 | opts.banner = "Run with #{$0} [options] user@server/resource password [host] [port]"
12 |
13 | opts.on('-D', '--debug', 'Run in debug mode (you will see all XMPP communication)') do
14 | options[:debug] = true
15 | end
16 |
17 | opts.on('-d', '--daemonize', 'Daemonize the process') do |daemonize|
18 | options[:daemonize] = daemonize
19 | end
20 |
21 | opts.on('--pid=[PID]', 'Write the PID to this file') do |pid|
22 | if !File.writable?(File.dirname(pid))
23 | $stderr.puts "Unable to write log file to #{pid}"
24 | exit 1
25 | end
26 | options[:pid] = pid
27 | end
28 |
29 | opts.on('--log=[LOG]', 'Write to the [LOG] file instead of stdout/stderr') do |log|
30 | if !File.writable?(File.dirname(log))
31 | $stderr.puts "Unable to write log file to #{log}"
32 | exit 1
33 | end
34 | options[:log] = log
35 | end
36 |
37 | opts.on('--certs=[CERTS DIRECTORY]', 'The directory path where the trusted certificates are stored') do |certs|
38 | if !File.directory?(certs)
39 | $stderr.puts "The certs directory path (#{certs}) is no good."
40 | exit 1
41 | end
42 | options[:certs] = certs
43 | end
44 |
45 | opts.on_tail('-h', '--help', 'Show this message') do
46 | puts opts
47 | exit
48 | end
49 |
50 | opts.on_tail('-v', '--version', 'Show version') do
51 | require 'yaml'
52 | version = YAML.load_file File.join(File.dirname(__FILE__), %w[.. .. VERSION.yml])
53 | puts "Blather v#{version[:major]}.#{version[:minor]}.#{version[:patch]}"
54 | exit
55 | end
56 | end
57 | optparse.parse!
58 |
59 | at_exit do
60 | unless client.setup?
61 | if ARGV.length < 2
62 | puts optparse
63 | exit 1
64 | end
65 | client.setup(*ARGV)
66 | end
67 |
68 | def at_exit_run(options)
69 | $stdin.reopen("/dev/null") if options[:daemonize] && $stdin.tty?
70 |
71 | if options[:log]
72 | log = File.new(options[:log], 'a')
73 | log.sync = options[:debug]
74 | $stdout.reopen log
75 | $stderr.reopen $stdout
76 | end
77 |
78 | Blather.logger.level = Logger::DEBUG if options[:debug]
79 |
80 | trap(:INT) { EM.stop }
81 | trap(:TERM) { EM.stop }
82 | EM.run { client.run }
83 | end
84 |
85 | if options[:daemonize]
86 | pid = fork do
87 | Process.setsid
88 | exit if fork
89 | File.open(options[:pid], 'w') { |f| f << Process.pid } if options[:pid]
90 | at_exit_run options
91 | FileUtils.rm(options[:pid]) if options[:pid]
92 | end
93 | ::Process.detach pid
94 | exit
95 | else
96 | at_exit_run options
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/blather/stanza/presence/subscription.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class Presence
4 |
5 | # # Subscription Stanza
6 | #
7 | # [RFC 3921 Section 8 - Integration of Roster Items and Presence Subscriptions](http://xmpp.org/rfcs/rfc3921.html#rfc.section.8)
8 | #
9 | # Blather handles subscription request/response through this class. It
10 | # provides a set of helper methods to quickly transform the stanza into a
11 | # response.
12 | #
13 | # @handler :subscription
14 | class Subscription < Presence
15 | register :subscription, :subscription
16 |
17 | # Create a new Subscription stanza
18 | #
19 | # @param [Blather::JID, #to_s] to the JID to subscribe to
20 | # @param [Symbol, nil] type the subscription type
21 | def self.new(to = nil, type = nil)
22 | node = super()
23 | node.to = to
24 | node.type = type
25 | node
26 | end
27 |
28 | module InstanceMethods
29 |
30 | # Set the to value on the stanza
31 | #
32 | # @param [Blather::JID, #to_s] to a JID to subscribe to
33 | def to=(to)
34 | super JID.new(to).stripped
35 | end
36 |
37 | # Transform the stanza into an approve stanza
38 | # makes approving requests simple
39 | #
40 | # @example approve an incoming request
41 | # subscription(:request?) { |s| write_to_stream s.approve! }
42 | # @return [self]
43 | def approve!
44 | self.type = :subscribed
45 | reply_if_needed!
46 | end
47 |
48 | # Transform the stanza into a refuse stanza
49 | # makes refusing requests simple
50 | #
51 | # @example refuse an incoming request
52 | # subscription(:request?) { |s| write_to_stream s.refuse! }
53 | # @return [self]
54 | def refuse!
55 | self.type = :unsubscribed
56 | reply_if_needed!
57 | end
58 |
59 | # Transform the stanza into an unsubscribe stanza
60 | # makes unsubscribing simple
61 | #
62 | # @return [self]
63 | def unsubscribe!
64 | self.type = :unsubscribe
65 | reply_if_needed!
66 | end
67 |
68 | # Transform the stanza into a cancel stanza
69 | # makes canceling simple
70 | #
71 | # @return [self]
72 | def cancel!
73 | self.type = :unsubscribed
74 | reply_if_needed!
75 | end
76 |
77 | # Transform the stanza into a request stanza
78 | # makes requests simple
79 | #
80 | # @return [self]
81 | def request!
82 | self.type = :subscribe
83 | reply_if_needed!
84 | end
85 |
86 | # Check if the stanza is a request
87 | #
88 | # @return [true, false]
89 | def request?
90 | self.type == :subscribe
91 | end
92 | end
93 |
94 | include InstanceMethods
95 |
96 | end #Subscription
97 |
98 | end #Presence
99 | end #Stanza
100 | end
101 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/subscribe_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub::Subscribe do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:subscribe, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Subscribe)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(subscribe_xml)).to be_instance_of Blather::Stanza::PubSub::Subscribe
11 | end
12 |
13 | it 'ensures an subscribe node is present on create' do
14 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node', 'jid'
15 | expect(subscribe.find('//ns:pubsub/ns:subscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
16 | end
17 |
18 | it 'ensures an subscribe node exists when calling #subscribe' do
19 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node', 'jid'
20 | subscribe.pubsub.remove_children :subscribe
21 | expect(subscribe.find('//ns:pubsub/ns:subscribe', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
22 |
23 | expect(subscribe.subscribe).not_to be_nil
24 | expect(subscribe.find('//ns:pubsub/ns:subscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'defaults to a set node' do
28 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node', 'jid'
29 | expect(subscribe.type).to eq(:set)
30 | end
31 |
32 | it 'sets the host if requested' do
33 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'pubsub.jabber.local', 'node', 'jid'
34 | expect(subscribe.to).to eq(Blather::JID.new('pubsub.jabber.local'))
35 | end
36 |
37 | it 'sets the node' do
38 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node-name', 'jid'
39 | expect(subscribe.node).to eq('node-name')
40 | end
41 |
42 | it 'has a node attribute' do
43 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node-name', 'jid'
44 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
45 | expect(subscribe.node).to eq('node-name')
46 |
47 | subscribe.node = 'new-node'
48 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@node="new-node"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
49 | expect(subscribe.node).to eq('new-node')
50 | end
51 |
52 | it 'has a jid attribute' do
53 | subscribe = Blather::Stanza::PubSub::Subscribe.new :set, 'host', 'node-name', 'jid'
54 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@jid="jid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
55 | expect(subscribe.jid).to eq(Blather::JID.new('jid'))
56 |
57 | subscribe.jid = Blather::JID.new('n@d/r')
58 | expect(subscribe.find('//ns:pubsub/ns:subscribe[@jid="n@d/r"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
59 | expect(subscribe.jid).to eq(Blather::JID.new('n@d/r'))
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/items.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Items Stanza
6 | #
7 | # [XEP-0060 Section 6.5 - Retrieve Items from a Node](http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve)
8 | #
9 | # @handler :pubsub_items
10 | class Items < PubSub
11 | register :pubsub_items, :items, self.registered_ns
12 |
13 | include Enumerable
14 | alias_method :find, :xpath
15 |
16 | # Create a new Items request
17 | #
18 | # @param [String] host the pubsub host to send the request to
19 | # @param [String] path the path of the node
20 | # @param [Array] list an array of IDs to request
21 | # @param [#to_s] max the maximum number of items to return
22 | #
23 | # @return [Blather::Stanza::PubSub::Items]
24 | def self.request(host, path, list = [], max = nil)
25 | node = self.new :get, host
26 |
27 | node.node = path
28 | node.max_items = max
29 |
30 | (list || []).each do |id|
31 | node.items_node << PubSubItem.new(id, nil, node.document)
32 | end
33 |
34 | node
35 | end
36 |
37 | # Overrides the parent to ensure an items node is created
38 | # @private
39 | def self.new(type = nil, host = nil)
40 | new_node = super
41 | new_node.items
42 | new_node
43 | end
44 |
45 | # Get the node name
46 | #
47 | # @return [String]
48 | def node
49 | items_node[:node]
50 | end
51 |
52 | # Set the node name
53 | #
54 | # @param [String, nil] node
55 | def node=(node)
56 | items_node[:node] = node
57 | end
58 |
59 | # Get the max number of items requested
60 | #
61 | # @return [Fixnum, nil]
62 | def max_items
63 | items_node[:max_items].to_i if items_node[:max_items]
64 | end
65 |
66 | # Set the max number of items requested
67 | #
68 | # @param [Fixnum, nil] max_items
69 | def max_items=(max_items)
70 | items_node[:max_items] = max_items
71 | end
72 |
73 | # Iterate over the list of items
74 | #
75 | # @yieldparam [Blather::Stanza::PubSub::PubSubItem] item
76 | def each(&block)
77 | items.each &block
78 | end
79 |
80 | # Get the list of items on this stanza
81 | #
82 | # @return [Array]
83 | def items
84 | items_node.find('ns:item', :ns => self.class.registered_ns).map do |i|
85 | PubSubItem.new(nil,nil,self.document).inherit i
86 | end
87 | end
88 |
89 | # Get or create the actual items node
90 | #
91 | # @return [Blather::XMPPNode]
92 | def items_node
93 | unless node = self.pubsub.find_first('ns:items', :ns => self.class.registered_ns)
94 | (self.pubsub << (node = XMPPNode.new('items', self.document)))
95 | node.namespace = self.pubsub.namespace
96 | end
97 | node
98 | end
99 | end # Items
100 |
101 | end # PubSub
102 | end # Stanza
103 | end # Blather
104 |
--------------------------------------------------------------------------------
/spec/blather/jid_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::JID do
4 | it 'does nothing if creaded from Blather::JID' do
5 | jid = Blather::JID.new 'n@d/r'
6 | expect(Blather::JID.new(jid).object_id).to eq(jid.object_id)
7 | end
8 |
9 | it 'creates a new Blather::JID from (n,d,r)' do
10 | jid = Blather::JID.new('n', 'd', 'r')
11 | expect(jid.node).to eq('n')
12 | expect(jid.domain).to eq('d')
13 | expect(jid.resource).to eq('r')
14 | end
15 |
16 | it 'creates a new Blather::JID from (n,d)' do
17 | jid = Blather::JID.new('n', 'd')
18 | expect(jid.node).to eq('n')
19 | expect(jid.domain).to eq('d')
20 | end
21 |
22 | it 'creates a new Blather::JID from (n@d)' do
23 | jid = Blather::JID.new('n@d')
24 | expect(jid.node).to eq('n')
25 | expect(jid.domain).to eq('d')
26 | end
27 |
28 | it 'creates a new Blather::JID from (n@d/r)' do
29 | jid = Blather::JID.new('n@d/r')
30 | expect(jid.node).to eq('n')
31 | expect(jid.domain).to eq('d')
32 | expect(jid.resource).to eq('r')
33 | end
34 |
35 | it 'requires at least a node' do
36 | expect { Blather::JID.new }.to raise_error ::ArgumentError
37 | end
38 |
39 | it 'ensures length of node is no more than 1023 characters' do
40 | expect { Blather::JID.new('n'*1024) }.to raise_error Blather::ArgumentError
41 | end
42 |
43 | it 'ensures length of domain is no more than 1023 characters' do
44 | expect { Blather::JID.new('n', 'd'*1024) }.to raise_error Blather::ArgumentError
45 | end
46 |
47 | it 'ensures length of resource is no more than 1023 characters' do
48 | expect { Blather::JID.new('n', 'd', 'r'*1024) }.to raise_error Blather::ArgumentError
49 | end
50 |
51 | it 'compares Blather::JIDs' do
52 | expect(Blather::JID.new('a@b/c') <=> Blather::JID.new('d@e/f')).to eq(-1)
53 | expect(Blather::JID.new('a@b/c') <=> Blather::JID.new('a@b/c')).to eq(0)
54 | expect(Blather::JID.new('d@e/f') <=> Blather::JID.new('a@b/c')).to eq(1)
55 | end
56 |
57 | it 'checks for equality' do
58 | expect(Blather::JID.new('n@d/r') == Blather::JID.new('n@d/r')).to eq(true)
59 | expect(Blather::JID.new('n@d/r').eql?(Blather::JID.new('n@d/r'))).to eq(true)
60 | end
61 |
62 | it 'will strip' do
63 | jid = Blather::JID.new('n@d/r')
64 | expect(jid.stripped).to eq(Blather::JID.new('n@d'))
65 | expect(jid).to eq(Blather::JID.new('n@d/r'))
66 | end
67 |
68 | it 'will strip itself' do
69 | jid = Blather::JID.new('n@d/r')
70 | jid.strip!
71 | expect(jid).to eq(Blather::JID.new('n@d'))
72 | end
73 |
74 | it 'has a string representation' do
75 | expect(Blather::JID.new('n@d/r').to_s).to eq('n@d/r')
76 | expect(Blather::JID.new('n', 'd', 'r').to_s).to eq('n@d/r')
77 | expect(Blather::JID.new('n', 'd').to_s).to eq('n@d')
78 | end
79 |
80 | it 'provides a #stripped? helper' do
81 | jid = Blather::JID.new 'a@b/c'
82 | expect(jid).to respond_to :stripped?
83 | expect(jid.stripped?).not_to equal true
84 | jid.strip!
85 | expect(jid.stripped?).to eq(true)
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/lib/blather/stanza/message/muc_user.rb:
--------------------------------------------------------------------------------
1 | require 'blather/stanza/muc/muc_user_base'
2 |
3 | module Blather
4 | class Stanza
5 | class Message
6 |
7 | class MUCUser < Message
8 | include Blather::Stanza::MUC::MUCUserBase
9 |
10 | register :muc_user_message, :x, "http://jabber.org/protocol/muc#user"
11 |
12 | def self.new(to = nil, body = nil, type = :normal)
13 | super
14 | end
15 |
16 | def invite?
17 | !!find_invite_node
18 | end
19 |
20 | def invite_decline?
21 | !!find_decline_node
22 | end
23 |
24 | def invite
25 | if invite = find_invite_node
26 | Invite.new invite
27 | else
28 | muc_user << (invite = Invite.new nil, nil, nil, self.document)
29 | invite
30 | end
31 | end
32 |
33 | def find_invite_node
34 | muc_user.find_first 'ns:invite', :ns => self.class.registered_ns
35 | end
36 |
37 | def decline
38 | if decline = find_decline_node
39 | Decline.new decline
40 | else
41 | muc_user << (decline = Decline.new nil, nil, nil, self.document)
42 | decline
43 | end
44 | end
45 |
46 | def find_decline_node
47 | muc_user.find_first 'ns:decline', :ns => self.class.registered_ns
48 | end
49 |
50 | class InviteBase < XMPPNode
51 | def self.new(element_name, to = nil, from = nil, reason = nil, document = nil)
52 | new_node = super element_name, document
53 |
54 | case to
55 | when self
56 | to.document ||= document
57 | return to
58 | when Nokogiri::XML::Node
59 | new_node.inherit to
60 | when Hash
61 | new_node.to = to[:to]
62 | new_node.from = to[:from]
63 | new_node.reason = to[:reason]
64 | else
65 | new_node.to = to
66 | new_node.from = from
67 | new_node.reason = reason
68 | end
69 | new_node
70 | end
71 |
72 | def to
73 | read_attr :to
74 | end
75 |
76 | def to=(val)
77 | write_attr :to, val
78 | end
79 |
80 | def from
81 | read_attr :from
82 | end
83 |
84 | def from=(val)
85 | write_attr :from, val
86 | end
87 |
88 | def reason
89 | reason_node.content.strip
90 | end
91 |
92 | def reason=(val)
93 | reason_node.content = val
94 | end
95 |
96 | def reason_node
97 | unless reason = find_first('ns:reason', :ns => MUCUser.registered_ns)
98 | self << (reason = XMPPNode.new('reason', self.document))
99 | end
100 | reason
101 | end
102 | end
103 |
104 | class Invite < InviteBase
105 | def self.new(*args)
106 | new_node = super :invite, *args
107 | end
108 | end
109 |
110 | class Decline < InviteBase
111 | def self.new(*args)
112 | new_node = super :decline, *args
113 | end
114 | end
115 | end # MUC
116 |
117 | end # Presence
118 | end # Stanza
119 | end # Blather
120 |
--------------------------------------------------------------------------------
/lib/blather.rb:
--------------------------------------------------------------------------------
1 | # Require the necessary files
2 | %w[
3 | rubygems
4 | eventmachine
5 | niceogiri
6 | ipaddr
7 | digest/md5
8 | digest/sha1
9 | logger
10 | openssl
11 |
12 | active_support/core_ext/class/attribute
13 | active_support/core_ext/object/blank
14 |
15 | blather/core_ext/eventmachine
16 | blather/core_ext/ipaddr
17 |
18 | blather/cert_store
19 | blather/errors
20 | blather/errors/sasl_error
21 | blather/errors/stanza_error
22 | blather/errors/stream_error
23 | blather/file_transfer
24 | blather/file_transfer/ibb
25 | blather/file_transfer/s5b
26 | blather/jid
27 | blather/roster
28 | blather/roster_item
29 | blather/xmpp_node
30 |
31 | blather/stanza
32 | blather/stanza/iq
33 | blather/stanza/iq/command
34 | blather/stanza/iq/ibb
35 | blather/stanza/iq/ping
36 | blather/stanza/iq/query
37 | blather/stanza/iq/ibr
38 | blather/stanza/iq/roster
39 | blather/stanza/iq/s5b
40 | blather/stanza/iq/si
41 | blather/stanza/iq/vcard
42 | blather/stanza/disco
43 | blather/stanza/disco/disco_info
44 | blather/stanza/disco/disco_items
45 | blather/stanza/disco/capabilities
46 | blather/stanza/message
47 | blather/stanza/message/muc_user
48 | blather/stanza/presence
49 | blather/stanza/presence/c
50 | blather/stanza/presence/status
51 | blather/stanza/presence/subscription
52 | blather/stanza/presence/muc
53 | blather/stanza/presence/muc_user
54 |
55 | blather/stanza/pubsub
56 | blather/stanza/pubsub/affiliations
57 | blather/stanza/pubsub/create
58 | blather/stanza/pubsub/event
59 | blather/stanza/pubsub/items
60 | blather/stanza/pubsub/publish
61 | blather/stanza/pubsub/retract
62 | blather/stanza/pubsub/subscribe
63 | blather/stanza/pubsub/subscription
64 | blather/stanza/pubsub/subscriptions
65 | blather/stanza/pubsub/unsubscribe
66 |
67 | blather/stanza/pubsub_owner
68 | blather/stanza/pubsub_owner/delete
69 | blather/stanza/pubsub_owner/purge
70 |
71 | blather/stanza/x
72 |
73 | blather/stream
74 | blather/stream/client
75 | blather/stream/component
76 | blather/stream/parser
77 | blather/stream/features
78 | blather/stream/features/resource
79 | blather/stream/features/sasl
80 | blather/stream/features/session
81 | blather/stream/features/tls
82 | blather/stream/features/register
83 | ].each { |r| require r }
84 |
85 | module Blather
86 | @@logger = nil
87 |
88 | class << self
89 |
90 | # Default logger level. Any internal call to log() will forward the log message to
91 | # the default log level
92 | attr_accessor :default_log_level
93 |
94 | def logger
95 | @@logger ||= Logger.new($stdout).tap {|logger| logger.level = Logger::INFO }
96 | end
97 |
98 | def logger=(logger)
99 | @@logger = logger
100 | end
101 |
102 | def default_log_level
103 | @default_log_level ||= :debug # by default is debug (as it used to be)
104 | end
105 |
106 | def log(message)
107 | logger.send self.default_log_level, message
108 | end
109 |
110 | end
111 |
112 | end
113 |
--------------------------------------------------------------------------------
/spec/blather/stanza/presence/muc_user_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def muc_user_xml
4 | <<-XML
5 |
8 |
9 |
12 |
13 |
14 | foobar
15 |
16 |
17 | XML
18 | end
19 |
20 | describe 'Blather::Stanza::Presence::MUCUser' do
21 | it 'must be importable' do
22 | muc_user = Blather::XMPPNode.parse(muc_user_xml)
23 | expect(muc_user).to be_kind_of Blather::Stanza::Presence::MUCUser::InstanceMethods
24 | expect(muc_user.affiliation).to eq(:none)
25 | expect(muc_user.jid).to eq('hag66@shakespeare.lit/pda')
26 | expect(muc_user.role).to eq(:participant)
27 | expect(muc_user.status_codes).to eq([100, 110])
28 | expect(muc_user.password).to eq('foobar')
29 | end
30 |
31 | it 'ensures a form node is present on create' do
32 | c = Blather::Stanza::Presence::MUCUser.new
33 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUCUser.registered_ns)).not_to be_empty
34 | end
35 |
36 | it 'ensures a form node exists when calling #muc' do
37 | c = Blather::Stanza::Presence::MUCUser.new
38 | c.remove_children :x
39 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUCUser.registered_ns)).to be_empty
40 |
41 | expect(c.muc_user).not_to be_nil
42 | expect(c.xpath('ns:x', :ns => Blather::Stanza::Presence::MUCUser.registered_ns)).not_to be_empty
43 | end
44 |
45 | it "must be able to set the affiliation" do
46 | muc_user = Blather::Stanza::Presence::MUCUser.new
47 | expect(muc_user.affiliation).to eq(nil)
48 | muc_user.affiliation = :none
49 | expect(muc_user.affiliation).to eq(:none)
50 | end
51 |
52 | it "must be able to set the role" do
53 | muc_user = Blather::Stanza::Presence::MUCUser.new
54 | expect(muc_user.role).to eq(nil)
55 | muc_user.role = :participant
56 | expect(muc_user.role).to eq(:participant)
57 | end
58 |
59 | it "must be able to set the jid" do
60 | muc_user = Blather::Stanza::Presence::MUCUser.new
61 | expect(muc_user.jid).to eq(nil)
62 | muc_user.jid = 'foo@bar.com'
63 | expect(muc_user.jid).to eq('foo@bar.com')
64 | end
65 |
66 | it "must be able to set the status codes" do
67 | muc_user = Blather::Stanza::Presence::MUCUser.new
68 | expect(muc_user.status_codes).to eq([])
69 | muc_user.status_codes = [100, 110]
70 | expect(muc_user.status_codes).to eq([100, 110])
71 | muc_user.status_codes = [500]
72 | expect(muc_user.status_codes).to eq([500])
73 | end
74 |
75 | it "must be able to set the password" do
76 | muc_user = Blather::Stanza::Presence::MUCUser.new
77 | expect(muc_user.password).to eq(nil)
78 | muc_user.password = 'barbaz'
79 | expect(muc_user.password).to eq('barbaz')
80 | muc_user.password = 'hello_world'
81 | expect(muc_user.password).to eq('hello_world')
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/retract_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub::Retract do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:retract, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Retract)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(retract_xml)).to be_instance_of Blather::Stanza::PubSub::Retract
11 | end
12 |
13 | it 'ensures an retract node is present on create' do
14 | retract = Blather::Stanza::PubSub::Retract.new
15 | expect(retract.find('//ns:pubsub/ns:retract', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
16 | end
17 |
18 | it 'ensures an retract node exists when calling #retract' do
19 | retract = Blather::Stanza::PubSub::Retract.new
20 | retract.pubsub.remove_children :retract
21 | expect(retract.find('//ns:pubsub/ns:retract', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
22 |
23 | expect(retract.retract).not_to be_nil
24 | expect(retract.find('//ns:pubsub/ns:retract', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'defaults to a set node' do
28 | retract = Blather::Stanza::PubSub::Retract.new
29 | expect(retract.type).to eq(:set)
30 | end
31 |
32 | it 'sets the host if requested' do
33 | retract = Blather::Stanza::PubSub::Retract.new 'pubsub.jabber.local'
34 | expect(retract.to).to eq(Blather::JID.new('pubsub.jabber.local'))
35 | end
36 |
37 | it 'sets the node' do
38 | retract = Blather::Stanza::PubSub::Retract.new 'host', 'node-name'
39 | expect(retract.node).to eq('node-name')
40 | end
41 |
42 | it 'can set the retractions as a string' do
43 | retract = Blather::Stanza::PubSub::Retract.new 'host', 'node'
44 | retract.retractions = 'id1'
45 | expect(retract.xpath('//ns:retract[ns:item[@id="id1"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
46 | end
47 |
48 | it 'can set the retractions as an array' do
49 | retract = Blather::Stanza::PubSub::Retract.new 'host', 'node'
50 | retract.retractions = %w[id1 id2]
51 | expect(retract.xpath('//ns:retract[ns:item[@id="id1"] and ns:item[@id="id2"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
52 | end
53 |
54 | it 'will iterate over each item' do
55 | retract = Blather::Stanza::PubSub::Retract.new.inherit parse_stanza(retract_xml).root
56 | expect(retract.retractions.size).to eq(1)
57 | expect(retract.size).to eq(retract.retractions.size)
58 | expect(retract.retractions).to eq(%w[ae890ac52d0df67ed7cfdf51b644e901])
59 | end
60 |
61 | it 'has a node attribute' do
62 | retract = Blather::Stanza::PubSub::Retract.new
63 | expect(retract).to respond_to :node
64 | expect(retract.node).to be_nil
65 | retract.node = 'node-name'
66 | expect(retract.node).to eq('node-name')
67 | expect(retract.xpath('//ns:retract[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
68 | end
69 |
70 | it 'will iterate over each retraction' do
71 | Blather::XMPPNode.parse(retract_xml).each do |i|
72 | expect(i).to include "ae890ac52d0df67ed7cfdf51b644e901"
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/subscriptions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | def control_subscriptions
5 | { :subscribed => [{:node => 'node1', :jid => 'francisco@denmark.lit', :subid => 'fd8237yr872h3f289j2'}, {:node => 'node2', :jid => 'francisco@denmark.lit', :subid => 'h8394hf8923ju'}],
6 | :unconfigured => [{:node => 'node3', :jid => 'francisco@denmark.lit'}],
7 | :pending => [{:node => 'node4', :jid => 'francisco@denmark.lit'}],
8 | :none => [{:node => 'node5', :jid => 'francisco@denmark.lit'}] }
9 | end
10 |
11 | describe Blather::Stanza::PubSub::Subscriptions do
12 | it 'registers itself' do
13 | expect(Blather::XMPPNode.class_from_registration(:subscriptions, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Subscriptions)
14 | end
15 |
16 | it 'can be imported' do
17 | expect(Blather::XMPPNode.parse(subscriptions_xml)).to be_instance_of Blather::Stanza::PubSub::Subscriptions
18 | end
19 |
20 | it 'ensures an subscriptions node is present on create' do
21 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new
22 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
23 | end
24 |
25 | it 'ensures an subscriptions node exists when calling #subscriptions' do
26 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new
27 | subscriptions.pubsub.remove_children :subscriptions
28 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
29 |
30 | expect(subscriptions.subscriptions).not_to be_nil
31 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
32 | end
33 |
34 | it 'ensures the subscriptions node is not duplicated when calling #subscriptions' do
35 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new
36 | subscriptions.pubsub.remove_children :subscriptions
37 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
38 |
39 | 5.times { subscriptions.subscriptions }
40 | expect(subscriptions.find('//ns:pubsub/ns:subscriptions', :ns => Blather::Stanza::PubSub.registered_ns).count).to eq(1)
41 | end
42 |
43 | it 'defaults to a get node' do
44 | aff = Blather::Stanza::PubSub::Subscriptions.new
45 | expect(aff.type).to eq(:get)
46 | end
47 |
48 | it 'sets the host if requested' do
49 | aff = Blather::Stanza::PubSub::Subscriptions.new :get, 'pubsub.jabber.local'
50 | expect(aff.to).to eq(Blather::JID.new('pubsub.jabber.local'))
51 | end
52 |
53 | it 'can import a subscriptions result node' do
54 | node = parse_stanza(subscriptions_xml).root
55 |
56 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new.inherit node
57 | expect(subscriptions.size).to eq(4)
58 | expect(subscriptions.list).to eq(control_subscriptions)
59 | end
60 |
61 | it 'will iterate over each subscription' do
62 | node = parse_stanza(subscriptions_xml).root
63 | subscriptions = Blather::Stanza::PubSub::Subscriptions.new.inherit node
64 | subscriptions.each do |type, nodes|
65 | expect(nodes).to eq(control_subscriptions[type])
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/blather/stream/parser.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stream
3 |
4 | # @private
5 | class Parser < Nokogiri::XML::SAX::Document
6 | @@debug = false
7 | def self.debug; @@debug; end
8 | def self.debug=(debug); @@debug = debug; end
9 |
10 | def initialize(receiver)
11 | @receiver = receiver
12 | @current = nil
13 | @namespaces = {}
14 | @namespace_definitions = []
15 | @parser = Nokogiri::XML::SAX::PushParser.new self
16 | @parser.options = Nokogiri::XML::ParseOptions::NOENT
17 | end
18 |
19 | def receive_data(string)
20 | Blather.log "PARSING: (#{string})" if @@debug
21 | @parser << string
22 | self
23 | rescue Nokogiri::XML::SyntaxError => e
24 | error e.message
25 | end
26 | alias_method :<<, :receive_data
27 |
28 | def start_element_namespace(elem, attrs, prefix, uri, namespaces)
29 | Blather.log "START ELEM: (#{{:elem => elem, :attrs => attrs, :prefix => prefix, :uri => uri, :ns => namespaces}.inspect})" if @@debug
30 |
31 | args = [elem]
32 | args << @current.document if @current
33 | node = XMPPNode.new *args
34 | node.document.root = node unless @current
35 |
36 | ns_keys = namespaces.map { |pre, href| pre }
37 | @namespace_definitions.push []
38 | namespaces.each do |pre, href|
39 | next if @namespace_definitions.flatten.include?(@namespaces[[pre, href]])
40 | ns = node.add_namespace(pre, href)
41 | @namespaces[[pre, href]] ||= ns
42 | end
43 | @namespaces[[prefix, uri]] ||= node.add_namespace(prefix, uri) if prefix && !ns_keys.include?(prefix)
44 | node.namespace = @namespaces[[prefix, uri]]
45 |
46 | attrs.each do |attr|
47 | node["#{attr.prefix + ':' if attr.prefix}#{attr.localname}"] = attr.value
48 | end
49 |
50 | unless @receiver.stopped?
51 | @current << node if @current
52 | @current = node
53 | end
54 |
55 | deliver(node) if elem == 'stream'
56 | end
57 |
58 | def end_element_namespace(elem, prefix, uri)
59 | Blather.log "END ELEM: #{{:elem => elem, :prefix => prefix, :uri => uri}.inspect}" if @@debug
60 |
61 | if elem == 'stream'
62 | node = XMPPNode.new('end')
63 | node.namespace = {prefix => uri}
64 | deliver node
65 | elsif @current.parent != @current.document
66 | @namespace_definitions.pop
67 | @current = @current.parent
68 | else
69 | deliver @current
70 | end
71 | end
72 |
73 | def characters(chars = '')
74 | Blather.log "CHARS: #{chars}" if @@debug
75 | @current << Nokogiri::XML::Text.new(chars, @current.document) if @current
76 | end
77 |
78 | def warning(msg)
79 | Blather.log "PARSE WARNING: #{msg}" if @@debug
80 | end
81 |
82 | def error(msg)
83 | raise ParseError.new(msg)
84 | end
85 |
86 | def finish
87 | @parser.finish
88 | rescue ParseError, RuntimeError
89 | end
90 |
91 | private
92 | def deliver(node)
93 | @current, @namespaces, @namespace_definitions = nil, {}, []
94 | @receiver.receive node
95 | end
96 | end #Parser
97 |
98 | end #Stream
99 | end #Blather
100 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/publish.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Publish Stanza
6 | #
7 | # [XEP-0060 Section 7.1 - Publish an Item to a Node](http://xmpp.org/extensions/xep-0060.html#publisher-publish)
8 | #
9 | # @handler :pubsub_publish
10 | class Publish < PubSub
11 | register :pubsub_publish, :publish, self.registered_ns
12 |
13 | include Enumerable
14 | alias_method :find, :xpath
15 |
16 | # Create a new publish node
17 | #
18 | # @param [String, nil] host the host to pushlish the node to
19 | # @param [String, nil] node the name of the node to publish to
20 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the node type
21 | # @param [#to_s] payload the payload to publish see {#payload=}
22 | def self.new(host = nil, node = nil, type = :set, payload = nil)
23 | new_node = super(type, host)
24 | new_node.node = node
25 | new_node.payload = payload if payload
26 | new_node
27 | end
28 |
29 | # Set the payload to publish
30 | #
31 | # @overload payload=(hash)
32 | # Set the payload as a set of ID => payload entries
33 | # @param [Hash payload>] hash
34 | # @overload payload=(array)
35 | # Set the list of payloads all at once
36 | # @param [Array<#to_s>] array
37 | # @overload payload=(string)
38 | # Set the payload as a string
39 | # @param [#to_s] string
40 | def payload=(payload)
41 | payload = case payload
42 | when Hash then payload.to_a
43 | when Array then payload.map { |v| [nil, v] }
44 | else [[nil, payload]]
45 | end
46 | payload.each do |id, value|
47 | self.publish << PubSubItem.new(id, value, self.document)
48 | end
49 | end
50 |
51 | # Get the name of the node to publish to
52 | #
53 | # @return [String, nil]
54 | def node
55 | publish[:node]
56 | end
57 |
58 | # Set the name of the node to publish to
59 | #
60 | # @param [String, nil] node
61 | def node=(node)
62 | publish[:node] = node
63 | end
64 |
65 | # Get or create the actual publish node
66 | #
67 | # @return [Blather::XMPPNode]
68 | def publish
69 | unless publish = pubsub.find_first('ns:publish', :ns => self.class.registered_ns)
70 | self.pubsub << (publish = XMPPNode.new('publish', self.document))
71 | publish.namespace = self.pubsub.namespace
72 | end
73 | publish
74 | end
75 |
76 | # Get the list of items
77 | #
78 | # @return [Array]
79 | def items
80 | publish.find('ns:item', :ns => self.class.registered_ns).map do |i|
81 | PubSubItem.new(nil,nil,self.document).inherit i
82 | end
83 | end
84 |
85 | # Iterate over the list of items
86 | #
87 | # @yield [item] a block to accept each item
88 | # @yieldparam [Blather::Stanza::PubSub::PubSubItem]
89 | def each(&block)
90 | items.each &block
91 | end
92 |
93 | # Get the size of the items list
94 | #
95 | # @return [Fixnum]
96 | def size
97 | items.size
98 | end
99 | end # Publish
100 |
101 | end # PubSub
102 | end # Stanza
103 | end # Blather
104 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/items_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub::Items do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:items, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Items)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(items_all_nodes_xml)).to be_instance_of Blather::Stanza::PubSub::Items
11 | end
12 |
13 | it 'ensures an items node is present on create' do
14 | items = Blather::Stanza::PubSub::Items.new
15 | expect(items.find('//ns:pubsub/ns:items', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
16 | end
17 |
18 | it 'ensures an items node exists when calling #items' do
19 | items = Blather::Stanza::PubSub::Items.new
20 | items.pubsub.remove_children :items
21 | expect(items.find('//ns:pubsub/ns:items', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
22 |
23 | expect(items.items).not_to be_nil
24 | expect(items.find('//ns:pubsub/ns:items', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'defaults to a get node' do
28 | aff = Blather::Stanza::PubSub::Items.new
29 | expect(aff.type).to eq(:get)
30 | end
31 |
32 | it 'ensures newly inherited items are PubSubItem objects' do
33 | items = Blather::XMPPNode.parse(items_all_nodes_xml)
34 | expect(items.map { |i| i.class }.uniq).to eq([Blather::Stanza::PubSub::PubSubItem])
35 | end
36 |
37 | it 'will iterate over each item' do
38 | n = parse_stanza items_all_nodes_xml
39 | items = Blather::Stanza::PubSub::Items.new.inherit n.root
40 | count = 0
41 | items.each { |i| expect(i).to be_instance_of Blather::Stanza::PubSub::PubSubItem; count += 1 }
42 | expect(count).to eq(4)
43 | end
44 |
45 | it 'can create an items request node to request all items' do
46 | host = 'pubsub.jabber.local'
47 | node = 'princely_musings'
48 |
49 | items = Blather::Stanza::PubSub::Items.request host, node
50 | expect(items.find("//ns:items[@node=\"#{node}\"]", :ns => Blather::Stanza::PubSub.registered_ns).size).to eq(1)
51 | expect(items.to).to eq(Blather::JID.new(host))
52 | expect(items.node).to eq(node)
53 | end
54 |
55 | it 'can create an items request node to request some items' do
56 | host = 'pubsub.jabber.local'
57 | node = 'princely_musings'
58 | items = %w[item1 item2]
59 |
60 | items_xpath = items.map { |i| "@id=\"#{i}\"" } * ' or '
61 |
62 | items = Blather::Stanza::PubSub::Items.request host, node, items
63 | expect(items.find("//ns:items[@node=\"#{node}\"]/ns:item[#{items_xpath}]", :ns => Blather::Stanza::PubSub.registered_ns).size).to eq(2)
64 | expect(items.to).to eq(Blather::JID.new(host))
65 | expect(items.node).to eq(node)
66 | end
67 |
68 | it 'can create an items request node to request "max_number" of items' do
69 | host = 'pubsub.jabber.local'
70 | node = 'princely_musings'
71 | max = 3
72 |
73 | items = Blather::Stanza::PubSub::Items.request host, node, nil, max
74 | expect(items.find("//ns:pubsub/ns:items[@node=\"#{node}\" and @max_items=\"#{max}\"]", :ns => Blather::Stanza::PubSub.registered_ns).size).to eq(1)
75 | expect(items.to).to eq(Blather::JID.new(host))
76 | expect(items.node).to eq(node)
77 | expect(items.max_items).to eq(max)
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/publish_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub::Publish do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:publish, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Publish)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(publish_xml)).to be_instance_of Blather::Stanza::PubSub::Publish
11 | end
12 |
13 | it 'ensures an publish node is present on create' do
14 | publish = Blather::Stanza::PubSub::Publish.new
15 | expect(publish.find('//ns:pubsub/ns:publish', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
16 | end
17 |
18 | it 'ensures an publish node exists when calling #publish' do
19 | publish = Blather::Stanza::PubSub::Publish.new
20 | publish.pubsub.remove_children :publish
21 | expect(publish.find('//ns:pubsub/ns:publish', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
22 |
23 | expect(publish.publish).not_to be_nil
24 | expect(publish.find('//ns:pubsub/ns:publish', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'defaults to a set node' do
28 | publish = Blather::Stanza::PubSub::Publish.new
29 | expect(publish.type).to eq(:set)
30 | end
31 |
32 | it 'sets the host if requested' do
33 | publish = Blather::Stanza::PubSub::Publish.new 'pubsub.jabber.local'
34 | expect(publish.to).to eq(Blather::JID.new('pubsub.jabber.local'))
35 | end
36 |
37 | it 'sets the node' do
38 | publish = Blather::Stanza::PubSub::Publish.new 'host', 'node-name'
39 | expect(publish.node).to eq('node-name')
40 | end
41 |
42 | it 'will iterate over each item' do
43 | publish = Blather::Stanza::PubSub::Publish.new.inherit parse_stanza(publish_xml).root
44 | count = 0
45 | publish.each do |i|
46 | expect(i).to be_instance_of Blather::Stanza::PubSub::PubSubItem
47 | count += 1
48 | end
49 | expect(count).to eq(1)
50 | end
51 |
52 | it 'has a node attribute' do
53 | publish = Blather::Stanza::PubSub::Publish.new
54 | expect(publish).to respond_to :node
55 | expect(publish.node).to be_nil
56 | publish.node = 'node-name'
57 | expect(publish.node).to eq('node-name')
58 | expect(publish.xpath('//ns:publish[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
59 | end
60 |
61 | it 'can set the payload with a hash' do
62 | payload = {'id1' => 'payload1', 'id2' => 'payload2'}
63 | publish = Blather::Stanza::PubSub::Publish.new
64 | publish.payload = payload
65 | expect(publish.size).to eq(2)
66 | expect(publish.xpath('/iq/ns:pubsub/ns:publish[ns:item[@id="id1" and .="payload1"] and ns:item[@id="id2" and .="payload2"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
67 | end
68 |
69 | it 'can set the payload with an array' do
70 | payload = %w[payload1 payload2]
71 | publish = Blather::Stanza::PubSub::Publish.new
72 | publish.payload = payload
73 | expect(publish.size).to eq(2)
74 | expect(publish.xpath('/iq/ns:pubsub/ns:publish[ns:item[.="payload1"] and ns:item[.="payload2"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
75 | end
76 |
77 | it 'can set the payload with a string' do
78 | publish = Blather::Stanza::PubSub::Publish.new
79 | publish.payload = 'payload'
80 | expect(publish.size).to eq(1)
81 | expect(publish.xpath('/iq/ns:pubsub/ns:publish[ns:item[.="payload"]]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/spec/blather/stanza/presence/subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::Stanza::Presence::Subscription do
4 | it 'registers itself' do
5 | expect(Blather::XMPPNode.class_from_registration(:subscription, nil)).to eq(Blather::Stanza::Presence::Subscription)
6 | end
7 |
8 | [:subscribe, :subscribed, :unsubscribe, :unsubscribed].each do |type|
9 | it "must be importable as #{type}" do
10 | expect(Blather::XMPPNode.parse("")).to be_kind_of Blather::Stanza::Presence::Subscription::InstanceMethods
11 | end
12 | end
13 |
14 | it 'can set to on creation' do
15 | sub = Blather::Stanza::Presence::Subscription.new 'a@b'
16 | expect(sub.to.to_s).to eq('a@b')
17 | end
18 |
19 | it 'can set a type on creation' do
20 | sub = Blather::Stanza::Presence::Subscription.new nil, :subscribed
21 | expect(sub.type).to eq(:subscribed)
22 | end
23 |
24 | it 'strips Blather::JIDs when setting #to' do
25 | sub = Blather::Stanza::Presence::Subscription.new 'a@b/c'
26 | expect(sub.to.to_s).to eq('a@b')
27 | end
28 |
29 | it 'generates an approval using #approve!' do
30 | sub = Blather::Stanza.import Nokogiri::XML('').root
31 | sub.approve!
32 | expect(sub.to).to eq('a@b')
33 | expect(sub.type).to eq(:subscribed)
34 | end
35 |
36 | it 'generates a refusal using #refuse!' do
37 | jid = Blather::JID.new 'a@b'
38 | sub = Blather::Stanza::Presence::Subscription.new
39 | sub.from = jid
40 | sub.refuse!
41 | expect(sub.to).to eq(jid)
42 | expect(sub.type).to eq(:unsubscribed)
43 | end
44 |
45 | it 'generates an unsubscript using #unsubscribe!' do
46 | jid = Blather::JID.new 'a@b'
47 | sub = Blather::Stanza::Presence::Subscription.new
48 | sub.from = jid
49 | sub.unsubscribe!
50 | expect(sub.to).to eq(jid)
51 | expect(sub.type).to eq(:unsubscribe)
52 | end
53 |
54 | it 'generates a cancellation using #cancel!' do
55 | jid = Blather::JID.new 'a@b'
56 | sub = Blather::Stanza::Presence::Subscription.new
57 | sub.from = jid
58 | sub.cancel!
59 | expect(sub.to).to eq(jid)
60 | expect(sub.type).to eq(:unsubscribed)
61 | end
62 |
63 | it 'generates a request using #request!' do
64 | jid = Blather::JID.new 'a@b'
65 | sub = Blather::Stanza::Presence::Subscription.new
66 | sub.from = jid
67 | sub.request!
68 | expect(sub.to).to eq(jid)
69 | expect(sub.type).to eq(:subscribe)
70 | end
71 |
72 | it 'has a #request? helper' do
73 | sub = Blather::Stanza::Presence::Subscription.new
74 | expect(sub).to respond_to :request?
75 | sub.type = :subscribe
76 | expect(sub.request?).to eq(true)
77 | end
78 |
79 | it "successfully routes chained actions" do
80 | from = Blather::JID.new("foo@bar.com")
81 | to = Blather::JID.new("baz@quux.com")
82 | sub = Blather::Stanza::Presence::Subscription.new
83 | sub.from = from
84 | sub.to = to
85 | sub.cancel!
86 | sub.unsubscribe!
87 | expect(sub.type).to eq(:unsubscribe)
88 | expect(sub.to).to eq(from)
89 | expect(sub.from).to eq(to)
90 | end
91 |
92 | it "will inherit only another node's attributes" do
93 | inheritable = Blather::XMPPNode.new 'foo'
94 | inheritable[:bar] = 'baz'
95 |
96 | sub = Blather::Stanza::Presence::Subscription.new
97 | expect(sub).to respond_to :inherit
98 |
99 | sub.inherit inheritable
100 | expect(sub[:bar]).to eq('baz')
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/spec/blather/stanza/iq/si_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def si_xml
4 | <<-XML
5 |
6 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | XML
26 | end
27 |
28 | describe Blather::Stanza::Iq::Si do
29 | it 'registers itself' do
30 | expect(Blather::XMPPNode.class_from_registration(:si, 'http://jabber.org/protocol/si')).to eq(Blather::Stanza::Iq::Si)
31 | end
32 |
33 | it 'can be imported' do
34 | node = Blather::XMPPNode.parse si_xml
35 | expect(node).to be_instance_of Blather::Stanza::Iq::Si
36 | expect(node.si).to be_instance_of Blather::Stanza::Iq::Si::Si
37 | end
38 |
39 | it 'ensures a si node is present on create' do
40 | iq = Blather::Stanza::Iq::Si.new
41 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si')).not_to be_empty
42 | end
43 |
44 | it 'ensures a si node exists when calling #si' do
45 | iq = Blather::Stanza::Iq::Si.new
46 | iq.si.remove
47 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si')).to be_empty
48 |
49 | expect(iq.si).not_to be_nil
50 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si')).not_to be_empty
51 | end
52 |
53 | it 'ensures a si node is replaced when calling #si=' do
54 | iq = Blather::XMPPNode.parse si_xml
55 |
56 | new_si = Blather::Stanza::Iq::Si::Si.new
57 | new_si.id = 'a1'
58 |
59 | iq.si = new_si
60 |
61 | expect(iq.xpath('ns:si', :ns => 'http://jabber.org/protocol/si').size).to eq(1)
62 | expect(iq.si.id).to eq('a1')
63 | end
64 | end
65 |
66 | describe Blather::Stanza::Iq::Si::Si do
67 | it 'can set and get attributes' do
68 | si = Blather::Stanza::Iq::Si::Si.new
69 | si.id = 'a1'
70 | si.mime_type = 'text/plain'
71 | si.profile = 'http://jabber.org/protocol/si/profile/file-transfer'
72 | expect(si.id).to eq('a1')
73 | expect(si.mime_type).to eq('text/plain')
74 | expect(si.profile).to eq('http://jabber.org/protocol/si/profile/file-transfer')
75 | end
76 | end
77 |
78 | describe Blather::Stanza::Iq::Si::Si::File do
79 | it 'can be initialized with name and size' do
80 | file = Blather::Stanza::Iq::Si::Si::File.new('test.txt', 123)
81 | expect(file.name).to eq('test.txt')
82 | expect(file.size).to eq(123)
83 | end
84 |
85 | it 'can be initialized with node' do
86 | node = Blather::XMPPNode.parse si_xml
87 |
88 | file = Blather::Stanza::Iq::Si::Si::File.new node.find_first('.//ns:file', :ns => 'http://jabber.org/protocol/si/profile/file-transfer')
89 | expect(file.name).to eq('test.txt')
90 | expect(file.size).to eq(1022)
91 | end
92 |
93 | it 'can set and get description' do
94 | file = Blather::Stanza::Iq::Si::Si::File.new('test.txt', 123)
95 | file.desc = 'This is a test. If this were a real file...'
96 | expect(file.desc).to eq('This is a test. If this were a real file...')
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/spec/blather/stanza/iq/vcard_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def vcard_xml
4 | <<-XML
5 |
6 |
7 | Romeo
8 |
9 |
10 | XML
11 | end
12 |
13 | describe Blather::Stanza::Iq::Vcard do
14 | it 'registers itself' do
15 | expect(Blather::XMPPNode.class_from_registration(:vCard, 'vcard-temp')).to eq(Blather::Stanza::Iq::Vcard)
16 | end
17 |
18 | it 'can be imported' do
19 | query = Blather::XMPPNode.parse vcard_xml
20 | expect(query).to be_instance_of Blather::Stanza::Iq::Vcard
21 | expect(query.vcard).to be_instance_of Blather::Stanza::Iq::Vcard::Vcard
22 | end
23 |
24 | it 'ensures a vcard node is present on create' do
25 | query = Blather::Stanza::Iq::Vcard.new
26 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp')).not_to be_empty
27 | end
28 |
29 | it 'ensures a vcard node exists when calling #vcard' do
30 | query = Blather::Stanza::Iq::Vcard.new
31 | query.vcard.remove
32 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp')).to be_empty
33 |
34 | expect(query.vcard).not_to be_nil
35 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp')).not_to be_empty
36 | end
37 |
38 | it 'ensures a vcard node is replaced when calling #vcard=' do
39 | query = Blather::XMPPNode.parse vcard_xml
40 |
41 | new_vcard = Blather::Stanza::Iq::Vcard::Vcard.new
42 | new_vcard["NICKNAME"] = 'Mercutio'
43 |
44 | query.vcard = new_vcard
45 |
46 | expect(query.xpath('ns:vCard', :ns => 'vcard-temp').size).to eq(1)
47 | expect(query.find_first('ns:vCard/ns:NICKNAME', :ns => 'vcard-temp').content).to eq('Mercutio')
48 | end
49 | end
50 |
51 | describe Blather::Stanza::Iq::Vcard::Vcard do
52 | it 'can set vcard elements' do
53 | query = Blather::Stanza::Iq::Vcard.new :set
54 | query.vcard['NICKNAME'] = 'Romeo'
55 | expect(query.find_first('ns:vCard/ns:NICKNAME', :ns => 'vcard-temp').content).to eq('Romeo')
56 | end
57 |
58 | it 'can set deep vcard elements' do
59 | query = Blather::Stanza::Iq::Vcard.new :set
60 | query.vcard['PHOTO/TYPE'] = 'image/png'
61 | query.vcard['PHOTO/BINVAL'] = '===='
62 | expect(query.find_first('ns:vCard/ns:PHOTO', :ns => 'vcard-temp').children.size).to eq(2)
63 | expect(query.find_first('ns:vCard/ns:PHOTO', :ns => 'vcard-temp').children.detect { |n| n.element_name == 'TYPE' && n.content == 'image/png' }).not_to be_nil
64 | expect(query.find_first('ns:vCard/ns:PHOTO', :ns => 'vcard-temp').children.detect { |n| n.element_name == 'BINVAL' && n.content == '====' }).not_to be_nil
65 | end
66 |
67 | it 'can get vcard elements' do
68 | query = Blather::Stanza::Iq::Vcard.new :set
69 | query.vcard['NICKNAME'] = 'Romeo'
70 | expect(query.vcard['NICKNAME']).to eq('Romeo')
71 | end
72 |
73 | it 'can get deep vcard elements' do
74 | query = Blather::Stanza::Iq::Vcard.new :set
75 | query.vcard['PHOTO/TYPE'] = 'image/png'
76 | query.vcard['PHOTO/BINVAL'] = '===='
77 | expect(query.vcard['PHOTO/TYPE']).to eq('image/png')
78 | expect(query.vcard['PHOTO/BINVAL']).to eq('====')
79 | end
80 |
81 | it 'returns nil on vcard elements which does not exist' do
82 | query = Blather::Stanza::Iq::Vcard.new :set
83 | query.vcard['NICKNAME'] = 'Romeo'
84 | expect(query.vcard['FN']).to be_nil
85 | end
86 |
87 | it 'can update vcard elements' do
88 | query = Blather::XMPPNode.parse vcard_xml
89 | expect(query.vcard['NICKNAME']).to eq('Romeo')
90 | query.vcard['NICKNAME'] = 'Mercutio'
91 | expect(query.vcard['NICKNAME']).to eq('Mercutio')
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/lib/blather/errors/stanza_error.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 |
3 | # Stanza errors
4 | # RFC3920 Section 9.3 (http://xmpp.org/rfcs/rfc3920.html#stanzas-error)
5 | #
6 | # @handler :stanza_error
7 | class StanzaError < BlatherError
8 | # @private
9 | STANZA_ERR_NS = 'urn:ietf:params:xml:ns:xmpp-stanzas'
10 | # @private
11 | VALID_TYPES = [:cancel, :continue, :modify, :auth, :wait].freeze
12 |
13 | register :stanza_error
14 |
15 | attr_reader :original, :name, :type, :text, :extras
16 |
17 | # Factory method for instantiating the proper class for the error
18 | #
19 | # @param [Blather::XMPPNode] node the error node to import
20 | # @return [Blather::StanzaError]
21 | def self.import(node)
22 | original = node.copy
23 | original.remove_child 'error'
24 |
25 | error_node = node.find_first '//*[local-name()="error"]'
26 |
27 | name = error_node.find_first('child::*[name()!="text"]', STANZA_ERR_NS).element_name
28 | type = error_node['type']
29 | text = node.find_first 'descendant::*[name()="text"]', STANZA_ERR_NS
30 | text = text.content if text
31 |
32 | extras = error_node.find("descendant::*[name()!='text' and name()!='#{name}']").map { |n| n }
33 |
34 | self.new original, name, type, text, extras
35 | end
36 |
37 | # Create a new StanzaError
38 | #
39 | # @param [Blather::XMPPNode] original the original stanza
40 | # @param [String] name the error name
41 | # @param [#to_s] type the error type as specified in
42 | # [RFC3920](http://xmpp.org/rfcs/rfc3920.html#rfc.section.9.3.2)
43 | # @param [String, nil] text additional text for the error
44 | # @param [Array] extras an array of extra nodes to add
45 | def initialize(original, name, type, text = nil, extras = [])
46 | @original = original
47 | @name = name
48 | self.type = type
49 | @text = text
50 | @extras = extras
51 | end
52 |
53 | # Set the error type
54 | #
55 | # @param [#to_sym] type the new error type. Must be on of
56 | # Blather::StanzaError::VALID_TYPES
57 | # @see [RFC3920 Section 9.3.2](http://xmpp.org/rfcs/rfc3920.html#rfc.section.9.3.2)
58 | def type=(type)
59 | type = type.to_sym
60 | if !VALID_TYPES.include?(type)
61 | raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
62 | end
63 | @type = type
64 | end
65 |
66 | # The error name
67 | #
68 | # @return [Symbol]
69 | def name
70 | @name.gsub('-','_').to_sym
71 | end
72 |
73 | # Creates an XML node from the error
74 | #
75 | # @return [Blather::XMPPNode]
76 | def to_node
77 | node = self.original.reply
78 | node.type = 'error'
79 | node << (error_node = XMPPNode.new('error'))
80 |
81 | error_node << (err = XMPPNode.new(@name, error_node.document))
82 | error_node['type'] = self.type
83 | err.namespace = 'urn:ietf:params:xml:ns:xmpp-stanzas'
84 |
85 | if self.text
86 | error_node << (text = XMPPNode.new('text', error_node.document))
87 | text.namespace = 'urn:ietf:params:xml:ns:xmpp-stanzas'
88 | text.content = self.text
89 | end
90 |
91 | self.extras.each { |extra| error_node << extra.dup }
92 | node
93 | end
94 |
95 | # Convert the object to a proper node then convert it to a string
96 | #
97 | # @return [String]
98 | def to_xml(*args)
99 | to_node.to_xml(*args)
100 | end
101 |
102 | # @private
103 | def inspect
104 | "Stanza Error (#{@name}): #{self.text} [#{self.extras}]"
105 | end
106 | # @private
107 | alias_method :to_s, :inspect
108 | end # StanzaError
109 |
110 | end # Blather
111 |
--------------------------------------------------------------------------------
/lib/blather/stanza/presence/muc_user.rb:
--------------------------------------------------------------------------------
1 | require 'blather/stanza/muc/muc_user_base'
2 |
3 | module Blather
4 | class Stanza
5 | class Presence
6 |
7 | class MUCUser < Presence
8 | include Blather::Stanza::MUC::MUCUserBase
9 |
10 | def self.decorator_modules
11 | super + [Blather::Stanza::MUC::MUCUserBase]
12 | end
13 |
14 | register :muc_user_presence, :x, MUC_USER_NAMESPACE
15 |
16 | module InstanceMethods
17 |
18 | def affiliation
19 | item.affiliation
20 | end
21 |
22 | def affiliation=(val)
23 | item.affiliation = val
24 | end
25 |
26 | def role
27 | item.role
28 | end
29 |
30 | def role=(val)
31 | item.role = val
32 | end
33 |
34 | def jid
35 | item.jid
36 | end
37 |
38 | def jid=(val)
39 | item.jid = val
40 | end
41 |
42 | def status_codes
43 | status.map &:code
44 | end
45 |
46 | def status_codes=(val)
47 | muc_user.remove_children :status
48 | val.each do |code|
49 | muc_user << Status.new(code)
50 | end
51 | end
52 |
53 | def item
54 | if item = muc_user.find_first('ns:item', :ns => MUCUser.registered_ns)
55 | Item.new item
56 | else
57 | muc_user << (item = Item.new nil, nil, nil, self.document)
58 | item
59 | end
60 | end
61 |
62 | def status
63 | muc_user.find('ns:status', :ns => MUCUser.registered_ns).map do |status|
64 | Status.new status
65 | end
66 | end
67 | end
68 |
69 | include InstanceMethods
70 |
71 | class Item < XMPPNode
72 | def self.new(affiliation = nil, role = nil, jid = nil, document = nil)
73 | new_node = super :item, document
74 |
75 | case affiliation
76 | when self
77 | affiliation.document ||= document
78 | return affiliation
79 | when Nokogiri::XML::Node
80 | new_node.inherit affiliation
81 | when Hash
82 | new_node.affiliation = affiliation[:affiliation]
83 | new_node.role = affiliation[:role]
84 | new_node.jid = affiliation[:jid]
85 | else
86 | new_node.affiliation = affiliation
87 | new_node.role = role
88 | new_node.jid = jid
89 | end
90 | new_node
91 | end
92 |
93 | def affiliation
94 | read_attr :affiliation, :to_sym
95 | end
96 |
97 | def affiliation=(val)
98 | write_attr :affiliation, val
99 | end
100 |
101 | def role
102 | read_attr :role, :to_sym
103 | end
104 |
105 | def role=(val)
106 | write_attr :role, val
107 | end
108 |
109 | def jid
110 | read_attr :jid
111 | end
112 |
113 | def jid=(val)
114 | write_attr :jid, val
115 | end
116 | end
117 |
118 | class Status < XMPPNode
119 | def self.new(code = nil)
120 | new_node = super :status
121 |
122 | case code
123 | when self.class
124 | return code
125 | when Nokogiri::XML::Node
126 | new_node.inherit code
127 | when Hash
128 | new_node.code = code[:code]
129 | else
130 | new_node.code = code
131 | end
132 | new_node
133 | end
134 |
135 | def code
136 | read_attr :code, :to_i
137 | end
138 |
139 | def code=(val)
140 | write_attr :code, val
141 | end
142 | end
143 | end # MUC
144 |
145 | end # Presence
146 | end # Stanza
147 | end # Blather
148 |
--------------------------------------------------------------------------------
/lib/blather/roster.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 |
3 | # Local Roster
4 | # Takes care of adding/removing JIDs through the stream
5 | class Roster
6 | include Enumerable
7 |
8 | attr_reader :version
9 |
10 | # Create a new roster
11 | #
12 | # @param [Blather::Stream] stream the stream the roster should use to
13 | # update roster entries
14 | # @param [Blather::Stanza::Iq::Roster] stanza a roster stanza used to preload
15 | # the roster
16 | # @return [Blather::Roster]
17 | def initialize(stream, stanza = nil)
18 | @stream = stream
19 | @items = {}
20 | process(stanza) if stanza
21 | end
22 |
23 | # Process any incoming stanzas and either adds or removes the
24 | # corresponding RosterItem
25 | #
26 | # @param [Blather::Stanza::Iq::Roster] stanza a roster stanza
27 | def process(stanza)
28 | @version = stanza.version
29 | stanza.items.each do |i|
30 | case i.subscription
31 | when :remove then @items.delete(key(i.jid))
32 | else @items[key(i.jid)] = RosterItem.new(i)
33 | end
34 | end
35 | end
36 |
37 | # Pushes a JID into the roster
38 | #
39 | # @param [String, Blather::JID, #jid] elem a JID to add to the roster
40 | # @return [self]
41 | # @see #push
42 | def <<(elem)
43 | push elem
44 | self
45 | end
46 |
47 | # Push a JID into the roster and update the server
48 | #
49 | # @param [String, Blather::JID, #jid] elem a jid to add to the roster
50 | # @param [true, false] send send the update over the wire
51 | # @see Blather::JID
52 | def push(elem, send = true)
53 | jid = elem.respond_to?(:jid) && elem.jid ? elem.jid : JID.new(elem)
54 | @items[key(jid)] = node = RosterItem.new(elem)
55 |
56 | @stream.write(node.to_stanza(:set)) if send
57 | end
58 | alias_method :add, :push
59 |
60 | # Remove a JID from the roster and update the server
61 | #
62 | # @param [String, Blather::JID] jid the JID to remove from the roster
63 | def delete(jid)
64 | @items.delete key(jid)
65 | item = Stanza::Iq::Roster::RosterItem.new(jid, nil, :remove)
66 | @stream.write Stanza::Iq::Roster.new(:set, item)
67 | end
68 | alias_method :remove, :delete
69 |
70 | # Get a RosterItem by JID
71 | #
72 | # @param [String, Blather::JID] jid the jid of the item to return
73 | # @return [Blather::RosterItem, nil] the associated RosterItem
74 | def [](jid)
75 | items[key(jid)]
76 | end
77 |
78 | # Iterate over all RosterItems
79 | #
80 | # @yield [Blather::RosterItem] yields each RosterItem
81 | def each(&block)
82 | items.values.each &block
83 | end
84 |
85 | # Get a duplicate of all RosterItems
86 | #
87 | # @return [Array] a duplicate of all RosterItems
88 | def items
89 | @items.dup
90 | end
91 |
92 | # Number of items in the roster
93 | #
94 | # @return [Integer] the number of items in the roster
95 | def length
96 | @items.length
97 | end
98 |
99 | # A hash of items keyed by group
100 | #
101 | # @return [Hash Array>]
102 | def grouped
103 | @items.values.sort.inject(Hash.new{|h,k|h[k]=[]}) do |hash, item|
104 | item.groups.each { |group| hash[group] << item }
105 | hash
106 | end
107 | end
108 |
109 | private
110 | # Creates a stripped jid
111 | def self.key(jid)
112 | JID.new(jid).stripped.to_s
113 | end
114 |
115 | # Instance method to wrap around the class method
116 | def key(jid)
117 | self.class.key(jid)
118 | end
119 | end # Roster
120 |
121 | end # Blather
122 |
--------------------------------------------------------------------------------
/spec/blather/roster_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::Roster do
4 | before do
5 | @stream = mock()
6 | @stream.stubs(:write)
7 |
8 | @stanza = mock()
9 | items = 4.times.map { |n| Blather::Stanza::Iq::Roster::RosterItem.new(jid: "n@d/#{n}r") }
10 | @stanza.stubs(:items).returns(items)
11 | @stanza.stubs(:version).returns('24d091d0dcfab1b3')
12 |
13 | @roster = Blather::Roster.new(@stream, @stanza)
14 | end
15 |
16 | it 'initializes with items' do
17 | expect(@roster.items.map { |_,i| i.jid.to_s }).to eq(@stanza.items.map { |i| i.jid.stripped.to_s }.uniq)
18 | end
19 |
20 | it 'processes @stanzas with remove requests' do
21 | s = @roster['n@d/0r']
22 | s.subscription = :remove
23 | expect { @roster.process(s.to_stanza) }.to change(@roster, :length).by -1
24 | end
25 |
26 | it 'processes @stanzas with add requests' do
27 | s = Blather::Stanza::Iq::Roster::RosterItem.new('a@b/c').to_stanza
28 | expect { @roster.process(s) }.to change(@roster, :length).by 1
29 | end
30 |
31 | it 'allows a jid to be pushed' do
32 | jid = 'a@b/c'
33 | expect { @roster.push(jid) }.to change(@roster, :length).by 1
34 | expect(@roster[jid]).not_to be_nil
35 | end
36 |
37 | it 'allows an item to be pushed' do
38 | jid = 'a@b/c'
39 | item = Blather::RosterItem.new(Blather::JID.new(jid))
40 | expect { @roster.push(item) }.to change(@roster, :length).by 1
41 | expect(@roster[jid]).not_to be_nil
42 | end
43 |
44 | it 'aliases #<< to #push and returns self to allow for chaining' do
45 | jid = 'a@b/c'
46 | item = Blather::RosterItem.new(Blather::JID.new(jid))
47 | jid2 = 'd@e/f'
48 | item2 = Blather::RosterItem.new(Blather::JID.new(jid2))
49 | expect { @roster << item << item2 }.to change(@roster, :length).by 2
50 | expect(@roster[jid]).not_to be_nil
51 | expect(@roster[jid2]).not_to be_nil
52 | end
53 |
54 | it 'sends a @roster addition over the wire' do
55 | client = mock(:write => nil)
56 | roster = Blather::Roster.new client, @stanza
57 | roster.push('a@b/c')
58 | end
59 |
60 | it 'removes a Blather::JID' do
61 | expect { @roster.delete 'n@d' }.to change(@roster, :length).by -1
62 | end
63 |
64 | it 'sends a @roster removal over the wire' do
65 | client = mock(:write => nil)
66 | roster = Blather::Roster.new client, @stanza
67 | roster.delete('a@b/c')
68 | end
69 |
70 | it 'returns an item through []' do
71 | item = @roster['n@d']
72 | expect(item).to be_kind_of Blather::RosterItem
73 | expect(item.jid).to eq(Blather::JID.new('n@d'))
74 | end
75 |
76 | it 'responds to #each' do
77 | expect(@roster).to respond_to :each
78 | end
79 |
80 | it 'cycles through all the items using #each' do
81 | expect(@roster.map { |i| i }.sort).to eq(@roster.items.values.sort)
82 | end
83 |
84 | it 'yields RosterItems from #each' do
85 | @roster.map { |i| expect(i).to be_kind_of Blather::RosterItem }
86 | end
87 |
88 | it 'returns a duplicate of items through #items' do
89 | items = @roster.items
90 | items.delete 'n@d'
91 | expect(items).not_to equal @roster.items
92 | end
93 |
94 | it 'will group roster items' do
95 | @roster.delete 'n@d'
96 | item1 = Blather::RosterItem.new("n1@d")
97 | item1.groups = ['group1', 'group2']
98 | item2 = Blather::RosterItem.new("n2@d")
99 | item2.groups = ['group1', 'group3']
100 | @roster << item1 << item2
101 |
102 | expect(@roster.grouped).to eq({
103 | 'group1' => [item1, item2],
104 | 'group2' => [item1],
105 | 'group3' => [item2]
106 | })
107 | end
108 |
109 | it 'has a version' do
110 | expect(@roster.version).to eq @stanza.version
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/spec/blather/stream/component_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::Stream::Component do
4 | let(:client) { mock 'Client' }
5 | let(:server_port) { 50000 - rand(1000) }
6 | let(:jid) { 'comp.id' }
7 |
8 | before do
9 | [:unbind, :post_init, :jid=].each do |m|
10 | client.stubs(m) unless client.respond_to?(m)
11 | end
12 | client.stubs(:jid).returns jid
13 | end
14 |
15 | def mocked_server(times = nil, &block)
16 | MockServer.any_instance.expects(:receive_data).send(*(times ? [:times, times] : [:at_least, 1])).with &block
17 | EventMachine::run {
18 | # Mocked server
19 | EventMachine::start_server '127.0.0.1', server_port, ServerMock
20 |
21 | # Blather::Stream connection
22 | EM.connect('127.0.0.1', server_port, Blather::Stream::Component, client, jid, 'secret') { |c| @stream = c }
23 | }
24 | end
25 |
26 | it 'can be started' do
27 | params = [client, 'comp.id', 'secret', 'host', 1234]
28 | EM.expects(:connect).with do |*parms|
29 | parms[0] == 'host' &&
30 | parms[1] == 1234 &&
31 | parms[3] == client &&
32 | parms[4] == 'comp.id'
33 | end
34 |
35 | Blather::Stream::Component.start *params
36 | end
37 |
38 | it 'shakes hands with the server' do
39 | state = nil
40 | mocked_server(2) do |val, server|
41 | case state
42 | when nil
43 | state = :started
44 | server.send_data ""
45 | expect(val).to match(/stream:stream/)
46 |
47 | when :started
48 | server.send_data ''
49 | EM.stop
50 | expect(val).to eq("#{Digest::SHA1.hexdigest('12345'+"secret")}")
51 |
52 | end
53 | end
54 | end
55 |
56 | it 'raises a NoConnection exception if the connection is unbound before it can be completed' do
57 | expect do
58 | EventMachine::run {
59 | EM.add_timer(0.5) { EM.stop if EM.reactor_running? }
60 |
61 | Blather::Stream::Component.start client, jid, 'pass', '127.0.0.1', 50000 - rand(1000)
62 | }
63 | end.to raise_error Blather::Stream::ConnectionFailed
64 | end
65 |
66 | it 'starts the stream once the connection is complete' do
67 | mocked_server(1) { |val, _| EM.stop; expect(val).to match(/stream:stream/) }
68 | end
69 |
70 | it 'sends stanzas to the client when the stream is ready' do
71 | client.stubs :post_init
72 | client.expects(:receive_data).with do |n|
73 | EM.stop
74 | n.kind_of? Blather::XMPPNode
75 | end
76 |
77 | state = nil
78 | mocked_server(2) do |val, server|
79 | case state
80 | when nil
81 | state = :started
82 | server.send_data ""
83 | expect(val).to match(/stream:stream/)
84 |
85 | when :started
86 | server.send_data ''
87 | server.send_data "Message!"
88 | expect(val).to eq("#{Digest::SHA1.hexdigest('12345'+"secret")}")
89 |
90 | end
91 | end
92 | end
93 |
94 | it 'sends stanzas to the wire ensuring "from" is set' do
95 | EM.expects(:next_tick).at_least(1).yields
96 |
97 | msg = Blather::Stanza::Message.new 'to@jid.com', 'body'
98 | comp = Blather::Stream::Component.new nil, client, 'jid.com', 'pass'
99 | comp.expects(:send_data).with { |s| expect(s).to match(/^]*from="jid\.com"/) }
100 | comp.send msg
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 |
4 | # # Pubsub Stanza
5 | #
6 | # [XEP-0060 - Publish-Subscribe](http://xmpp.org/extensions/xep-0060.html)
7 | #
8 | # The base class for all PubSub nodes. This provides helper methods common to
9 | # all PubSub nodes.
10 | #
11 | # @handler :pubsub_node
12 | class PubSub < Iq
13 | register :pubsub_node, :pubsub, 'http://jabber.org/protocol/pubsub'
14 |
15 | # @private
16 | def self.import(node)
17 | klass = nil
18 | if pubsub = node.document.find_first('//ns:pubsub', :ns => self.registered_ns)
19 | pubsub.children.detect do |e|
20 | ns = e.namespace ? e.namespace.href : nil
21 | klass = class_from_registration(e.element_name, ns)
22 | end
23 | end
24 | (klass || self).new(node[:type]).inherit(node)
25 | end
26 |
27 | # Overwrites the parent constructor to ensure a pubsub node is present.
28 | # Also allows the addition of a host attribute
29 | #
30 | # @param [] type the IQ type
31 | # @param [String, nil] host the host the node should be sent to
32 | def self.new(type = nil, host = nil)
33 | new_node = super type
34 | new_node.to = host
35 | new_node.pubsub
36 | new_node
37 | end
38 |
39 | # Overrides the parent to ensure the current pubsub node is destroyed before
40 | # inheritting the new content
41 | #
42 | # @private
43 | def inherit(node)
44 | remove_children :pubsub
45 | super
46 | end
47 |
48 | # Get or create the pubsub node on the stanza
49 | #
50 | # @return [Blather::XMPPNode]
51 | def pubsub
52 | p = find_first('ns:pubsub', :ns => self.class.registered_ns) ||
53 | find_first('pubsub', :ns => self.class.registered_ns)
54 |
55 | unless p
56 | self << (p = XMPPNode.new('pubsub', self.document))
57 | p.namespace = self.class.registered_ns
58 | end
59 | p
60 | end
61 | end # PubSub
62 |
63 | # # PubSubItem Fragment
64 | #
65 | # This fragment is found in many places throughout the pubsub spec
66 | # This is a convenience class to attach methods to the node
67 | class PubSubItem < XMPPNode
68 | # Create a new PubSubItem
69 | #
70 | # @param [String, nil] id the id of the stanza
71 | # @param [#to_s, nil] payload the payload to attach to this item.
72 | # @param [XML::Document, nil] document the document the node should be
73 | # attached to. This should be the document of the parent PubSub node.
74 | def self.new(id = nil, payload = nil, document = nil)
75 | return id if id.class == self
76 |
77 | new_node = super 'item', document
78 | new_node.id = id
79 | new_node.payload = payload if payload
80 | new_node
81 | end
82 |
83 | # Get the item's ID
84 | #
85 | # @return [String, nil]
86 | def id
87 | read_attr :id
88 | end
89 |
90 | # Set the item's ID
91 | #
92 | # @param [#to_s] id the new ID
93 | def id=(id)
94 | write_attr :id, id
95 | end
96 |
97 | alias_method :payload_node, :child
98 |
99 | # Get the item's payload
100 | #
101 | # @return [String, nil]
102 | def payload
103 | children.empty? ? nil : children.to_s
104 | end
105 |
106 | # Set the item's payload
107 | #
108 | # @param [String, XMPPNode, nil] payload the payload
109 | def payload=(payload)
110 | children.map &:remove
111 | return unless payload
112 | if payload.is_a?(String)
113 | self.content = payload
114 | else
115 | self << payload
116 | end
117 | end
118 | end # PubSubItem
119 |
120 | end # Stanza
121 | end # Blather
122 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/unsubscribe_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub::Unsubscribe do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:unsubscribe, 'http://jabber.org/protocol/pubsub')).to eq(Blather::Stanza::PubSub::Unsubscribe)
7 | end
8 |
9 | it 'can be imported' do
10 | expect(Blather::XMPPNode.parse(unsubscribe_xml)).to be_instance_of Blather::Stanza::PubSub::Unsubscribe
11 | end
12 |
13 | it 'ensures an unsubscribe node is present on create' do
14 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node', 'jid'
15 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
16 | end
17 |
18 | it 'ensures an unsubscribe node exists when calling #unsubscribe' do
19 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node', 'jid'
20 | unsubscribe.pubsub.remove_children :unsubscribe
21 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
22 |
23 | expect(unsubscribe.unsubscribe).not_to be_nil
24 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'defaults to a set node' do
28 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node', 'jid'
29 | expect(unsubscribe.type).to eq(:set)
30 | end
31 |
32 | it 'sets the host if requested' do
33 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'pubsub.jabber.local', 'node', 'jid'
34 | expect(unsubscribe.to).to eq(Blather::JID.new('pubsub.jabber.local'))
35 | end
36 |
37 | it 'sets the node' do
38 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid'
39 | expect(unsubscribe.node).to eq('node-name')
40 | end
41 |
42 | it 'has a node attribute' do
43 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid'
44 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@node="node-name"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
45 | expect(unsubscribe.node).to eq('node-name')
46 |
47 | unsubscribe.node = 'new-node'
48 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@node="new-node"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
49 | expect(unsubscribe.node).to eq('new-node')
50 | end
51 |
52 | it 'has a jid attribute' do
53 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid'
54 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@jid="jid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
55 | expect(unsubscribe.jid).to eq(Blather::JID.new('jid'))
56 |
57 | unsubscribe.jid = Blather::JID.new('n@d/r')
58 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@jid="n@d/r"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
59 | expect(unsubscribe.jid).to eq(Blather::JID.new('n@d/r'))
60 | end
61 |
62 | it 'has a subid attribute' do
63 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid'
64 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@subid="subid"]', :ns => Blather::Stanza::PubSub.registered_ns)).to be_empty
65 |
66 | unsubscribe = Blather::Stanza::PubSub::Unsubscribe.new :set, 'host', 'node-name', 'jid', 'subid'
67 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@subid="subid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
68 | expect(unsubscribe.subid).to eq('subid')
69 |
70 | unsubscribe.subid = 'newsubid'
71 | expect(unsubscribe.find('//ns:pubsub/ns:unsubscribe[@subid="newsubid"]', :ns => Blather::Stanza::PubSub.registered_ns)).not_to be_empty
72 | expect(unsubscribe.subid).to eq('newsubid')
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/spec/blather/errors/stream_error_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def stream_error_node(error = 'internal-server-error', msg = nil)
4 | node = Blather::XMPPNode.new('error')
5 | node.namespace = {'stream' => Blather::Stream::STREAM_NS}
6 |
7 | node << (err = Blather::XMPPNode.new(error, node.document))
8 | err.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
9 |
10 | if msg
11 | node << (text = Blather::XMPPNode.new('text', node.document))
12 | text.namespace = 'urn:ietf:params:xml:ns:xmpp-streams'
13 | text.content = msg
14 | end
15 |
16 | node << (extra = Blather::XMPPNode.new('extra-error', node.document))
17 | extra.namespace = 'blather:stream:error'
18 | extra.content = 'Blather Error'
19 |
20 | node
21 | end
22 |
23 | describe 'Blather::StreamError' do
24 | it 'can import a node' do
25 | err = stream_error_node 'internal-server-error', 'the message'
26 | expect(Blather::StreamError).to respond_to :import
27 | e = Blather::StreamError.import err
28 | expect(e).to be_kind_of Blather::StreamError
29 |
30 | expect(e.name).to eq(:internal_server_error)
31 | expect(e.text).to eq('the message')
32 | expect(e.extras).to eq(err.find('descendant::*[name()="extra-error"]', 'blather:stream:error').map {|n|n})
33 | end
34 | end
35 |
36 | describe 'Blather::StreamError when instantiated' do
37 | before do
38 | @err_name = 'internal-server-error'
39 | @msg = 'the server has experienced a misconfiguration'
40 | @err = Blather::StreamError.import stream_error_node(@err_name, @msg)
41 | end
42 |
43 | it 'provides a err_name attribute' do
44 | expect(@err).to respond_to :name
45 | expect(@err.name).to eq(@err_name.gsub('-','_').to_sym)
46 | end
47 |
48 | it 'provides a text attribute' do
49 | expect(@err).to respond_to :text
50 | expect(@err.text).to eq(@msg)
51 | end
52 |
53 | it 'provides an extras attribute' do
54 | expect(@err).to respond_to :extras
55 | expect(@err.extras).to be_instance_of Array
56 | expect(@err.extras.size).to eq(1)
57 | expect(@err.extras.first.element_name).to eq('extra-error')
58 | end
59 |
60 | it 'describes itself' do
61 | expect(@err.to_s).to match(/#{@type}/)
62 | expect(@err.to_s).to match(/#{@msg}/)
63 |
64 | expect(@err.inspect).to match(/#{@type}/)
65 | expect(@err.inspect).to match(/#{@msg}/)
66 | end
67 |
68 | it 'can be turned into xml' do
69 | expect(@err).to respond_to :to_xml
70 | doc = parse_stanza @err.to_xml
71 | expect(doc.xpath("//err_ns:internal-server-error", :err_ns => Blather::StreamError::STREAM_ERR_NS)).not_to be_empty
72 | expect(doc.xpath("//err_ns:text[.='the server has experienced a misconfiguration']", :err_ns => Blather::StreamError::STREAM_ERR_NS)).not_to be_empty
73 | expect(doc.xpath("//err_ns:extra-error[.='Blather Error']", :err_ns => 'blather:stream:error')).not_to be_empty
74 | end
75 | end
76 |
77 | describe 'Each XMPP stream error type' do
78 | %w[ bad-format
79 | bad-namespace-prefix
80 | conflict
81 | connection-timeout
82 | host-gone
83 | host-unknown
84 | improper-addressing
85 | internal-server-error
86 | invalid-from
87 | invalid-id
88 | invalid-namespace
89 | invalid-xml
90 | not-authorized
91 | policy-violation
92 | remote-connection-failed
93 | resource-constraint
94 | restricted-xml
95 | see-other-host
96 | system-shutdown
97 | undefined-condition
98 | unsupported-encoding
99 | unsupported-stanza-type
100 | unsupported-version
101 | xml-not-well-formed
102 | ].each do |error_type|
103 | it "handles the name for #{error_type}" do
104 | e = Blather::StreamError.import stream_error_node(error_type)
105 | expect(e.name).to eq(error_type.gsub('-','_').to_sym)
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/lib/blather/file_transfer/s5b.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class FileTransfer
3 | # SOCKS5 Bytestreams Transfer helper
4 | # Takes care of accepting, declining and offering file transfers through the stream
5 | class S5b
6 |
7 | # Set this to false if you don't want to fallback to In-Band Bytestreams
8 | attr_accessor :allow_ibb_fallback
9 |
10 | # Set this to true if the buddies of your bot will be in the same local network
11 | #
12 | # Usually IM clients advertise all network addresses which they can determine.
13 | # Skipping the local ones can save time if your bot is not in the same local network as it's buddies
14 | attr_accessor :allow_private_ips
15 |
16 | def initialize(stream, iq)
17 | @stream = stream
18 | @iq = iq
19 | @allow_ibb_fallback = true
20 | @allow_private_ips = false
21 | end
22 |
23 | # Accept an incoming file-transfer
24 | #
25 | # @param [module] handler the handler for incoming data, see Blather::FileTransfer::SimpleFileReceiver for an example
26 | # @param [Array] params the params to be passed into the handler
27 | def accept(handler, *params)
28 | @streamhosts = @iq.streamhosts
29 | @streamhosts.delete_if {|s| begin IPAddr.new(s.host).private? rescue false end } unless @allow_private_ips
30 | @socket_address = Digest::SHA1.hexdigest("#{@iq.sid}#{@iq.from}#{@iq.to}")
31 |
32 | @handler = handler
33 | @params = params
34 |
35 | connect_next_streamhost
36 | @stream.clear_handlers :s5b_open, :from => @iq.from
37 | end
38 |
39 | # Decline an incoming file-transfer
40 | def decline
41 | @stream.clear_handlers :s5b_open, :from => @iq.from
42 | @stream.write StanzaError.new(@iq, 'not-acceptable', :auth).to_node
43 | end
44 |
45 | # Offer a file to somebody, not implemented yet
46 | def offer
47 | # TODO: implement
48 | end
49 |
50 | private
51 |
52 | def connect_next_streamhost
53 | if streamhost = @streamhosts.shift
54 | connect(streamhost)
55 | else
56 | if @allow_ibb_fallback
57 | @stream.register_handler :ibb_open, :from => @iq.from, :sid => @iq.sid do |iq|
58 | transfer = Blather::FileTransfer::Ibb.new(@stream, iq)
59 | transfer.accept(@handler, *@params)
60 | true
61 | end
62 | end
63 |
64 | @stream.write StanzaError.new(@iq, 'item-not-found', :cancel).to_node
65 | end
66 | end
67 |
68 | def connect(streamhost)
69 | begin
70 | socket = EM.connect streamhost.host, streamhost.port, SocketConnection, @socket_address, 0, @handler, *@params
71 |
72 | socket.callback do
73 | answer = @iq.reply
74 | answer.streamhosts = nil
75 | answer.streamhost_used = streamhost.jid
76 |
77 | @stream.write answer
78 | end
79 |
80 | socket.errback do
81 | connect_next_streamhost
82 | end
83 | rescue EventMachine::ConnectionError => e
84 | connect_next_streamhost
85 | end
86 | end
87 |
88 | # @private
89 | class SocketConnection < EM::P::Socks5
90 | include EM::Deferrable
91 |
92 | def initialize(host, port, handler, *params)
93 | super(host, port)
94 | @@handler = handler
95 | @params = params
96 | end
97 |
98 | def post_init
99 | self.succeed
100 |
101 | class << self
102 | include @@handler
103 | end
104 | send(:initialize, *@params)
105 | post_init
106 | end
107 |
108 | def unbind
109 | self.fail if @socks_state != :connected
110 | end
111 | end
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/lib/blather/xmpp_node.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 |
3 | # Base XML Node
4 | # All XML classes subclass XMPPNode it allows the addition of helpers
5 | class XMPPNode < Niceogiri::XML::Node
6 | # @private
7 | BASE_NAMES = %w[presence message iq].freeze
8 |
9 | # @private
10 | @@registrations = {}
11 |
12 | class_attribute :registered_ns, :registered_name
13 |
14 | # Register a new stanza class to a name and/or namespace
15 | #
16 | # This registers a namespace that is used when looking
17 | # up the class name of the object to instantiate when a new
18 | # stanza is received
19 | #
20 | # @param [#to_s] name the name of the node
21 | # @param [String, nil] ns the namespace the node belongs to
22 | def self.register(name, ns = nil)
23 | self.registered_name = name.to_s
24 | self.registered_ns = ns
25 | @@registrations[[self.registered_name, self.registered_ns]] = self
26 | end
27 |
28 | # Find the class to use given the name and namespace of a stanza
29 | #
30 | # @param [#to_s] name the name to lookup
31 | # @param [String, nil] xmlns the namespace the node belongs to
32 | # @return [Class, nil] the class appropriate for the name/ns combination
33 | def self.class_from_registration(name, ns = nil)
34 | reg = @@registrations[[name.to_s, ns]]
35 | return @@registrations[[name.to_s, nil]] if !reg && ["jabber:client", "jabber:component:accept"].include?(ns)
36 |
37 | reg
38 | end
39 |
40 | # Import an XML::Node to the appropriate class
41 | #
42 | # Looks up the class the node should be then creates it based on the
43 | # elements of the XML::Node
44 | # @param [XML::Node] node the node to import
45 | # @return the appropriate object based on the node name and namespace
46 | def self.import(node, *decorators)
47 | ns = (node.namespace.href if node.namespace)
48 | klass = class_from_registration(node.element_name, ns)
49 | if klass && klass != self
50 | klass.import(node, *decorators)
51 | else
52 | new(node.element_name).decorate(*decorators).inherit(node)
53 | end
54 | end
55 |
56 | # Parse a string as XML and import to the appropriate class
57 | #
58 | # @param [String] string the string to parse
59 | # @return the appropriate object based on the node name and namespace
60 | def self.parse(string)
61 | import Nokogiri::XML(string).root
62 | end
63 |
64 | # Create a new Node object
65 | #
66 | # @param [String, nil] name the element name
67 | # @param [XML::Document, nil] doc the document to attach the node to. If
68 | # not provided one will be created
69 | # @return a new object with the registered name and namespace
70 | def self.new(name = registered_name, doc = nil)
71 | super name, doc, BASE_NAMES.include?(name.to_s) ? nil : self.registered_ns
72 | end
73 |
74 | def self.decorator_modules
75 | if self.const_defined?(:InstanceMethods)
76 | [self::InstanceMethods]
77 | else
78 | []
79 | end
80 | end
81 |
82 | def decorate(*decorators)
83 | decorators.each do |decorator|
84 | decorator.decorator_modules.each do |mod|
85 | extend mod
86 | end
87 |
88 | @handler_hierarchy.unshift decorator.handler_hierarchy.first if decorator.respond_to?(:handler_hierarchy)
89 | end
90 | self
91 | end
92 |
93 | def content_from(name, ns = nil)
94 | content = super
95 | if !content && !ns
96 | return super("ns:#{name}", ns: "jabber:client") || super("ns:#{name}", ns: "jabber:component:accept")
97 | end
98 |
99 | content
100 | end
101 |
102 | # Turn the object into a proper stanza
103 | #
104 | # @return a stanza object
105 | def to_stanza
106 | self.class.import self
107 | end
108 | end # XMPPNode
109 |
110 | end # Blather
111 |
--------------------------------------------------------------------------------
/spec/blather/stanza/iq/ibb_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def ibb_open_xml
4 | <<-XML
5 |
9 |
13 |
14 | XML
15 | end
16 |
17 | def ibb_data_xml
18 | <<-XML
19 |
23 |
24 | qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ
25 | WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu
26 | IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P
27 | AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH
28 | kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA
29 |
30 |
31 | XML
32 | end
33 |
34 | def ibb_close_xml
35 | <<-XML
36 |
40 |
41 |
42 | XML
43 | end
44 |
45 | describe Blather::Stanza::Iq::Ibb::Open do
46 | it 'registers itself' do
47 | expect(Blather::XMPPNode.class_from_registration(:open, 'http://jabber.org/protocol/ibb')).to eq(Blather::Stanza::Iq::Ibb::Open)
48 | end
49 |
50 | it 'can be imported' do
51 | node = Blather::XMPPNode.parse ibb_open_xml
52 | expect(node).to be_instance_of Blather::Stanza::Iq::Ibb::Open
53 | end
54 |
55 | it 'has open node' do
56 | node = Blather::XMPPNode.parse ibb_open_xml
57 | expect(node.open).to be_kind_of Nokogiri::XML::Element
58 | end
59 |
60 | it 'can get sid' do
61 | node = Blather::XMPPNode.parse ibb_open_xml
62 | expect(node.sid).to eq('i781hf64')
63 | end
64 |
65 | it 'deleted open node on reply' do
66 | node = Blather::XMPPNode.parse ibb_open_xml
67 | reply = node.reply
68 | expect(reply.open).to be_nil
69 | end
70 | end
71 |
72 | describe Blather::Stanza::Iq::Ibb::Data do
73 | it 'registers itself' do
74 | expect(Blather::XMPPNode.class_from_registration(:data, 'http://jabber.org/protocol/ibb')).to eq(Blather::Stanza::Iq::Ibb::Data)
75 | end
76 |
77 | it 'can be imported' do
78 | node = Blather::XMPPNode.parse ibb_data_xml
79 | expect(node).to be_instance_of Blather::Stanza::Iq::Ibb::Data
80 | end
81 |
82 | it 'has data node' do
83 | node = Blather::XMPPNode.parse ibb_data_xml
84 | expect(node.data).to be_kind_of Nokogiri::XML::Element
85 | end
86 |
87 | it 'can get sid' do
88 | node = Blather::XMPPNode.parse ibb_data_xml
89 | expect(node.sid).to eq('i781hf64')
90 | end
91 |
92 | it 'deleted data node on reply' do
93 | node = Blather::XMPPNode.parse ibb_data_xml
94 | reply = node.reply
95 | expect(reply.data).to be_nil
96 | end
97 | end
98 |
99 | describe Blather::Stanza::Iq::Ibb::Close do
100 | it 'registers itself' do
101 | expect(Blather::XMPPNode.class_from_registration(:close, 'http://jabber.org/protocol/ibb')).to eq(Blather::Stanza::Iq::Ibb::Close)
102 | end
103 |
104 | it 'can be imported' do
105 | node = Blather::XMPPNode.parse ibb_close_xml
106 | expect(node).to be_instance_of Blather::Stanza::Iq::Ibb::Close
107 | end
108 |
109 | it 'has close node' do
110 | node = Blather::XMPPNode.parse ibb_close_xml
111 | expect(node.close).to be_kind_of Nokogiri::XML::Element
112 | end
113 |
114 | it 'can get sid' do
115 | node = Blather::XMPPNode.parse ibb_close_xml
116 | expect(node.sid).to eq('i781hf64')
117 | end
118 |
119 | it 'deleted close node on reply' do
120 | node = Blather::XMPPNode.parse ibb_close_xml
121 | reply = node.reply
122 | expect(reply.close).to be_nil
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/event.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Event Stanza
6 | #
7 | # [XEP-0060](http://xmpp.org/extensions/xep-0060.html)
8 | #
9 | # The PubSub Event stanza is used in many places. Please see the XEP for more
10 | # information.
11 | #
12 | # @handler :pubsub_event
13 | class Event < Message
14 | # @private
15 | SHIM_NS = 'http://jabber.org/protocol/shim'.freeze
16 |
17 | register :pubsub_event, :event, 'http://jabber.org/protocol/pubsub#event'
18 |
19 | # Ensures the event_node is created
20 | # @private
21 | def self.new(type = nil)
22 | node = super
23 | node.event_node
24 | node
25 | end
26 |
27 | # Kill the event_node node before running inherit
28 | # @private
29 | def inherit(node)
30 | event_node.remove
31 | super
32 | end
33 |
34 | # Get the name of the node
35 | #
36 | # @return [String, nil]
37 | def node
38 | !purge? ? items_node[:node] : purge_node[:node]
39 | end
40 |
41 | # Get a list of retractions
42 | #
43 | # @return [Array]
44 | def retractions
45 | items_node.find('//ns:retract', :ns => self.class.registered_ns).map do |i|
46 | i[:id]
47 | end
48 | end
49 |
50 | # Check if this is a retractions stanza
51 | #
52 | # @return [Boolean]
53 | def retractions?
54 | !retractions.empty?
55 | end
56 |
57 | # Get the list of items attached to this event
58 | #
59 | # @return [Array]
60 | def items
61 | items_node.find('//ns:item', :ns => self.class.registered_ns).map do |i|
62 | PubSubItem.new(nil,nil,self.document).inherit i
63 | end
64 | end
65 |
66 | # Check if this stanza has items
67 | #
68 | # @return [Boolean]
69 | def items?
70 | !items.empty?
71 | end
72 |
73 | # Check if this is a purge stanza
74 | #
75 | # @return [XML::Node, nil]
76 | def purge?
77 | purge_node
78 | end
79 |
80 | # Get or create the actual event node
81 | #
82 | # @return [Blather::XMPPNode]
83 | def event_node
84 | node = find_first('//ns:event', :ns => self.class.registered_ns)
85 | node = find_first('//event', self.class.registered_ns) unless node
86 | unless node
87 | (self << (node = XMPPNode.new('event', self.document)))
88 | node.namespace = self.class.registered_ns
89 | end
90 | node
91 | end
92 |
93 | # Get or create the actual items node
94 | #
95 | # @return [Blather::XMPPNode]
96 | def items_node
97 | node = find_first('ns:event/ns:items', :ns => self.class.registered_ns)
98 | unless node
99 | (self.event_node << (node = XMPPNode.new('items', self.document)))
100 | node.namespace = event_node.namespace
101 | end
102 | node
103 | end
104 |
105 | # Get the actual purge node
106 | #
107 | # @return [Blather::XMPPNode]
108 | def purge_node
109 | event_node.find_first('//ns:purge', :ns => self.class.registered_ns)
110 | end
111 |
112 | # Get the subscription IDs associated with this event
113 | #
114 | # @return [Array]
115 | def subscription_ids
116 | find('//ns:header[@name="SubID"]', :ns => SHIM_NS).map do |n|
117 | n.content
118 | end
119 | end
120 |
121 | # Check if this is a subscription stanza
122 | #
123 | # @return [XML::Node, nil]
124 | def subscription?
125 | subscription_node
126 | end
127 |
128 | # Get the actual subscription node
129 | #
130 | # @return [Blather::XMPPNode]
131 | def subscription_node
132 | event_node.find_first('//ns:subscription', :ns => self.class.registered_ns)
133 | end
134 | alias_method :subscription, :subscription_node
135 | end # Event
136 |
137 | end # PubSub
138 | end # Stanza
139 | end # Blather
140 |
--------------------------------------------------------------------------------
/lib/blather/file_transfer.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | # File Transfer helper
3 | # Takes care of accepting, declining and offering file transfers through the stream
4 | class FileTransfer
5 |
6 | # Set this to false if you don't want to use In-Band Bytestreams
7 | attr_accessor :allow_ibb
8 |
9 | # Set this to false if you don't want to use SOCKS5 Bytestreams
10 | attr_accessor :allow_s5b
11 |
12 | # Set this to true if you want SOCKS5 Bytestreams to attempt to use private network addresses
13 | attr_accessor :allow_private_ips
14 |
15 | # Create a new FileTransfer
16 | #
17 | # @param [Blather::Stream] stream the stream the file transfer should use
18 | # @param [Blather::Stanza::Iq::Si] iq a si iq used to stream-initiation
19 | def initialize(stream, iq = nil)
20 | @stream = stream
21 | @allow_s5b = true
22 | @allow_ibb = true
23 |
24 | Blather.logger.debug "File transfers on the local network are ignored by default. Set #allow_private_ips = true if you need local network file transfers."
25 |
26 | @iq = iq
27 | end
28 |
29 | # Accept an incoming file-transfer
30 | #
31 | # @param [module] handler the handler for incoming data, see Blather::FileTransfer::SimpleFileReceiver for an example
32 | # @param [Array] params the params to be passed into the handler
33 | def accept(handler, *params)
34 | answer = @iq.reply
35 |
36 | answer.si.feature.x.type = :submit
37 |
38 | supported_methods = @iq.si.feature.x.field("stream-method").options.map(&:value)
39 | if supported_methods.include?(Stanza::Iq::S5b::NS_S5B) and @allow_s5b
40 | answer.si.feature.x.fields = {:var => 'stream-method', :value => Stanza::Iq::S5b::NS_S5B}
41 |
42 | @stream.register_handler :s5b_open, :from => @iq.from do |iq|
43 | transfer = Blather::FileTransfer::S5b.new(@stream, iq)
44 | transfer.allow_ibb_fallback = true if @allow_ibb
45 | transfer.allow_private_ips = true if @allow_private_ips
46 | EM.next_tick { transfer.accept(handler, *params) }
47 | true
48 | end
49 |
50 | @stream.write answer
51 | elsif supported_methods.include?(Stanza::Iq::Ibb::NS_IBB) and @allow_ibb
52 | answer.si.feature.x.fields = {:var => 'stream-method', :value => Stanza::Iq::Ibb::NS_IBB}
53 |
54 | @stream.register_handler :ibb_open, :from => @iq.from do |iq|
55 | transfer = Blather::FileTransfer::Ibb.new(@stream, iq)
56 | EM.next_tick { transfer.accept(handler, *params) }
57 | true
58 | end
59 |
60 | @stream.write answer
61 | else
62 | reason = XMPPNode.new('no-valid-streams')
63 | reason.namespace = Blather::Stanza::Iq::Si::NS_SI
64 |
65 | @stream.write StanzaError.new(@iq, 'bad-request', 'cancel', nil, [reason]).to_node
66 | end
67 | end
68 |
69 | # Decline an incoming file-transfer
70 | def decline
71 | answer = StanzaError.new(@iq, 'forbidden', 'cancel', 'Offer declined').to_node
72 |
73 | @stream.write answer
74 | end
75 |
76 | # Offer a file to somebody, not implemented yet
77 | def offer
78 | # TODO: implement
79 | end
80 |
81 | # Simple handler for incoming file transfers
82 | #
83 | # You can define your own handler and pass it to the accept method.
84 | module SimpleFileReceiver
85 | def initialize(path, size)
86 | @path = path
87 | @size = size
88 | @transferred = 0
89 | end
90 |
91 | # @private
92 | def post_init
93 | @file = File.open(@path, "w")
94 | end
95 |
96 | # @private
97 | def receive_data(data)
98 | @transferred += data.size
99 | @file.write data
100 | end
101 |
102 | # @private
103 | def unbind
104 | @file.close
105 | File.delete(@path) unless @transferred == @size
106 | end
107 | end
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/spec/blather/stanza/pubsub/event_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'fixtures/pubsub'
3 |
4 | describe Blather::Stanza::PubSub::Event do
5 | it 'registers itself' do
6 | expect(Blather::XMPPNode.class_from_registration(:event, 'http://jabber.org/protocol/pubsub#event')).to eq(Blather::Stanza::PubSub::Event)
7 | end
8 |
9 | it 'is importable' do
10 | expect(Blather::XMPPNode.parse(event_notification_xml)).to be_instance_of Blather::Stanza::PubSub::Event
11 | end
12 |
13 | it 'ensures a query node is present on create' do
14 | evt = Blather::Stanza::PubSub::Event.new
15 | expect(evt.find('ns:event', :ns => Blather::Stanza::PubSub::Event.registered_ns)).not_to be_empty
16 | end
17 |
18 | it 'ensures an event node exists when calling #event_node' do
19 | evt = Blather::Stanza::PubSub::Event.new
20 | evt.remove_children :event
21 | expect(evt.find('*[local-name()="event"]')).to be_empty
22 |
23 | expect(evt.event_node).not_to be_nil
24 | expect(evt.find('ns:event', :ns => Blather::Stanza::PubSub::Event.registered_ns)).not_to be_empty
25 | end
26 |
27 | it 'ensures an items node exists when calling #items_node' do
28 | evt = Blather::Stanza::PubSub::Event.new
29 | evt.remove_children :items
30 | expect(evt.find('*[local-name()="items"]')).to be_empty
31 |
32 | expect(evt.items_node).not_to be_nil
33 | expect(evt.find('ns:event/ns:items', :ns => Blather::Stanza::PubSub::Event.registered_ns)).not_to be_empty
34 | end
35 |
36 | it 'knows the associated node name' do
37 | evt = Blather::XMPPNode.parse(event_with_payload_xml)
38 | expect(evt.node).to eq('princely_musings')
39 | end
40 |
41 | it 'ensures newly inherited items are PubSubItem objects' do
42 | evt = Blather::XMPPNode.parse(event_with_payload_xml)
43 | expect(evt.items?).to eq(true)
44 | expect(evt.retractions?).to eq(false)
45 | expect(evt.items.map { |i| i.class }.uniq).to eq([Blather::Stanza::PubSub::PubSubItem])
46 | end
47 |
48 | it 'will iterate over each item' do
49 | evt = Blather::XMPPNode.parse(event_with_payload_xml)
50 | evt.items.each { |i| expect(i.class).to eq(Blather::Stanza::PubSub::PubSubItem) }
51 | end
52 |
53 | it 'handles receiving subscription ids' do
54 | evt = Blather::XMPPNode.parse(event_subids_xml)
55 | expect(evt.subscription_ids).to eq(['123-abc', '004-yyy'])
56 | end
57 |
58 | it 'can have a list of retractions' do
59 | evt = Blather::XMPPNode.parse(<<-NODE)
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | NODE
68 | expect(evt.retractions?).to eq(true)
69 | expect(evt.items?).to eq(false)
70 | expect(evt.retractions).to eq(%w[ae890ac52d0df67ed7cfdf51b644e901])
71 | end
72 |
73 | it 'can be a purge' do
74 | evt = Blather::XMPPNode.parse(<<-NODE)
75 |
76 |
77 |
78 |
79 |
80 | NODE
81 | expect(evt.purge?).not_to be_nil
82 | expect(evt.node).to eq('princely_musings')
83 | end
84 |
85 | it 'can be a subscription notification' do
86 | evt = Blather::XMPPNode.parse(<<-NODE)
87 |
88 |
89 |
90 |
91 |
92 | NODE
93 | expect(evt.subscription?).not_to be_nil
94 | expect(evt.subscription[:jid]).to eq('francisco@denmark.lit')
95 | expect(evt.subscription[:subscription]).to eq('subscribed')
96 | expect(evt.subscription[:node]).to eq('/example.com/test')
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/blather/core_ext/eventmachine.rb:
--------------------------------------------------------------------------------
1 | # @private
2 | module EventMachine
3 | # @private
4 | module Protocols
5 | # Basic SOCKS v5 client implementation
6 | #
7 | # Use as you would any regular connection:
8 | #
9 | # class MyConn < EM::P::Socks5
10 | # def post_init
11 | # send_data("sup")
12 | # end
13 | #
14 | # def receive_data(data)
15 | # send_data("you said: #{data}")
16 | # end
17 | # end
18 | #
19 | # EM.connect socks_host, socks_port, MyConn, host, port
20 | #
21 | # @private
22 | class Socks5 < Connection
23 | def initialize(host, port)
24 | @host = host
25 | @port = port
26 | @socks_error_code = nil
27 | @buffer = ''
28 | @socks_state = :method_negotiation
29 | @socks_methods = [0] # TODO: other authentication methods
30 | setup_methods
31 | end
32 |
33 | def setup_methods
34 | class << self
35 | def post_init; socks_post_init; end
36 | def receive_data(*a); socks_receive_data(*a); end
37 | end
38 | end
39 |
40 | def restore_methods
41 | class << self
42 | remove_method :post_init
43 | remove_method :receive_data
44 | end
45 | end
46 |
47 | def socks_post_init
48 | packet = [5, @socks_methods.size].pack('CC') + @socks_methods.pack('C*')
49 | send_data(packet)
50 | end
51 |
52 | def socks_receive_data(data)
53 | @buffer << data
54 |
55 | if @socks_state == :method_negotiation
56 | return if @buffer.size < 2
57 |
58 | header_resp = @buffer.slice! 0, 2
59 | _, method_code = header_resp.unpack("cc")
60 |
61 | if @socks_methods.include?(method_code)
62 | @socks_state = :connecting
63 | packet = [5, 1, 0].pack("C*")
64 |
65 | if @host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # IPv4
66 | packet << [1, $1.to_i, $2.to_i, $3.to_i, $4.to_i].pack("C*")
67 | elsif @host.include?(":") # IPv6
68 | l, r = if @host =~ /^(.*)::(.*)$/
69 | [$1,$2].map {|i| i.split ":"}
70 | else
71 | [@host.split(":"),[]]
72 | end
73 | dec_groups = (l + Array.new(8-l.size-r.size, '0') + r).map {|i| i.hex}
74 | packet << ([4] + dec_groups).pack("Cn8")
75 | else # Domain
76 | packet << [3, @host.length, @host].pack("CCA*")
77 | end
78 | packet << [@port].pack("n")
79 |
80 | send_data packet
81 | else
82 | @socks_state = :invalid
83 | @socks_error_code = method_code
84 | close_connection
85 | return
86 | end
87 | elsif @socks_state == :connecting
88 | return if @buffer.size < 4
89 |
90 | header_resp = @buffer.slice! 0, 4
91 | _, response_code, _, address_type = header_resp.unpack("C*")
92 |
93 | if response_code == 0
94 | case address_type
95 | when 1
96 | @buffer.slice! 0, 4
97 | when 3
98 | len = @buffer.slice! 0, 1
99 | @buffer.slice! 0, len.unpack("C").first
100 | when 4
101 | @buffer.slice! 0, 16
102 | else
103 | @socks_state = :invalid
104 | @socks_error_code = address_type
105 | close_connection
106 | return
107 | end
108 | @buffer.slice! 0, 2
109 |
110 | @socks_state = :connected
111 | restore_methods
112 |
113 | post_init
114 | receive_data(@buffer) unless @buffer.empty?
115 | else
116 | @socks_state = :invalid
117 | @socks_error_code = response_code
118 | close_connection
119 | return
120 | end
121 | end
122 | end
123 | end
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/lib/blather/stanza/pubsub/subscription.rb:
--------------------------------------------------------------------------------
1 | module Blather
2 | class Stanza
3 | class PubSub
4 |
5 | # # PubSub Subscription Stanza
6 | #
7 | # [XEP-0060 Section 8.8 Manage Subscriptions](http://xmpp.org/extensions/xep-0060.html#owner-subscriptions)
8 | #
9 | # @handler :pubsub_subscription
10 | class Subscription < PubSub
11 | # @private
12 | VALID_TYPES = [:none, :pending, :subscribed, :unconfigured]
13 |
14 | register :pubsub_subscription, :subscription, self.registered_ns
15 |
16 | # Create a new subscription request node
17 | #
18 | # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ type
19 | # @param [String] host the host to send the request to
20 | # @param [String] node the node to look for requests on
21 | # @param [Blather::JID, #to_s] jid the JID of the subscriber
22 | # @param [String] subid the subscription ID
23 | # @param [VALID_TYPES] subscription the subscription type
24 | def self.new(type = :result, host = nil, node = nil, jid = nil, subid = nil, subscription = nil)
25 | new_node = super(type, host)
26 | new_node.node = node
27 | new_node.jid = jid
28 | new_node.subid = subid
29 | new_node.subscription = subscription
30 | new_node
31 | end
32 |
33 | # Check if the type is none
34 | #
35 | # @return [Boolean]
36 | def none?
37 | self.subscription == :none
38 | end
39 |
40 | # Check if the type is pending
41 | #
42 | # @return [Boolean]
43 | def pending?
44 | self.subscription == :pending
45 | end
46 |
47 | # Check if the type is subscribed
48 | #
49 | # @return [Boolean]
50 | def subscribed?
51 | self.subscription == :subscribed
52 | end
53 |
54 | # Check if the type is unconfigured
55 | #
56 | # @return [Boolean]
57 | def unconfigured?
58 | self.subscription == :unconfigured
59 | end
60 |
61 | # Get the JID of the subscriber
62 | #
63 | # @return [Blather::JID]
64 | def jid
65 | JID.new(subscription_node[:jid])
66 | end
67 |
68 | # Set the JID of the subscriber
69 | #
70 | # @param [Blather::JID, #to_s] jid
71 | def jid=(jid)
72 | subscription_node[:jid] = jid
73 | end
74 |
75 | # Get the name of the subscription node
76 | #
77 | # @return [String]
78 | def node
79 | subscription_node[:node]
80 | end
81 |
82 | # Set the name of the subscription node
83 | #
84 | # @param [String] node
85 | def node=(node)
86 | subscription_node[:node] = node
87 | end
88 |
89 | # Get the ID of the subscription
90 | #
91 | # @return [String]
92 | def subid
93 | subscription_node[:subid]
94 | end
95 |
96 | # Set the ID of the subscription
97 | #
98 | # @param [String] subid
99 | def subid=(subid)
100 | subscription_node[:subid] = subid
101 | end
102 |
103 | # Get the subscription type
104 | #
105 | # @return [VALID_TYPES, nil]
106 | def subscription
107 | s = subscription_node[:subscription]
108 | s.to_sym if s
109 | end
110 |
111 | # Set the subscription type
112 | #
113 | # @param [VALID_TYPES, nil] subscription
114 | def subscription=(subscription)
115 | if subscription && !VALID_TYPES.include?(subscription.to_sym)
116 | raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
117 | end
118 | subscription_node[:subscription] = subscription
119 | end
120 |
121 | # Get or create the actual subscription node
122 | #
123 | # @return [Blather::XMPPNode]
124 | def subscription_node
125 | unless subscription = pubsub.find_first('ns:subscription', :ns => self.class.registered_ns)
126 | self.pubsub << (subscription = XMPPNode.new('subscription', self.document))
127 | subscription.namespace = self.pubsub.namespace
128 | end
129 | subscription
130 | end
131 | end # Subscribe
132 |
133 | end # PubSub
134 | end # Stanza
135 | end # Blather
136 |
--------------------------------------------------------------------------------
/spec/blather/roster_item_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Blather::RosterItem do
4 | it 'can be initialized with Blather::JID' do
5 | jid = Blather::JID.new(jid)
6 | i = Blather::RosterItem.new jid
7 | expect(i.jid).to eq(jid)
8 | end
9 |
10 | it 'can be initialized with an Iq::RosterItem' do
11 | jid = 'n@d/r'
12 | i = Blather::RosterItem.new Blather::Stanza::Iq::Roster::RosterItem.new(jid)
13 | expect(i.jid).to eq(Blather::JID.new(jid).stripped)
14 | end
15 |
16 | it 'can be initialized with a string' do
17 | jid = 'n@d/r'
18 | i = Blather::RosterItem.new jid
19 | expect(i.jid).to eq(Blather::JID.new(jid).stripped)
20 | end
21 |
22 | it 'returns the same object when intialized with a Blather::RosterItem' do
23 | control = Blather::RosterItem.new 'n@d/r'
24 | expect(Blather::RosterItem.new(control)).to be control
25 | end
26 |
27 | it 'has a Blather::JID setter that strips the Blather::JID' do
28 | jid = Blather::JID.new('n@d/r')
29 | i = Blather::RosterItem.new nil
30 | i.jid = jid
31 | expect(i.jid).to eq(jid.stripped)
32 | end
33 |
34 | it 'has a subscription setter that forces a symbol' do
35 | i = Blather::RosterItem.new nil
36 | i.subscription = 'remove'
37 | expect(i.subscription).to eq(:remove)
38 | end
39 |
40 | it 'forces the type of subscription' do
41 | expect { Blather::RosterItem.new(nil).subscription = 'foo' }.to raise_error Blather::ArgumentError
42 | end
43 |
44 | it 'returns :none if the subscription field is blank' do
45 | expect(Blather::RosterItem.new(nil).subscription).to eq(:none)
46 | end
47 |
48 | it 'ensure #ask is a symbol' do
49 | i = Blather::RosterItem.new(nil)
50 | i.ask = 'subscribe'
51 | expect(i.ask).to eq(:subscribe)
52 | end
53 |
54 | it 'forces #ask to be :subscribe or nothing at all' do
55 | expect { Blather::RosterItem.new(nil).ask = 'foo' }.to raise_error Blather::ArgumentError
56 | end
57 |
58 | it 'generates a stanza with #to_stanza' do
59 | jid = Blather::JID.new('n@d/r')
60 | i = Blather::RosterItem.new jid
61 | s = i.to_stanza
62 | expect(s).to be_kind_of Blather::Stanza::Iq::Roster
63 | expect(s.items.first.jid).to eq(jid.stripped)
64 | end
65 |
66 | it 'returns status based on priority' do
67 | setup_item_with_presences
68 | expect(@i.status).to eq(@p3)
69 | end
70 |
71 | it 'returns status based on priority and state' do
72 | setup_item_with_presences
73 |
74 | @p4 = Blather::Stanza::Presence::Status.new
75 | @p4.type = :unavailable
76 | @p4.from = 'n@d/d'
77 | @p4.priority = 15
78 | @i.status = @p4
79 |
80 | expect(@i.status).to eq(@p3)
81 | end
82 |
83 | it 'returns status based on resource' do
84 | setup_item_with_presences
85 | expect(@i.status('a')).to eq(@p)
86 | end
87 |
88 | def setup_item_with_presences
89 | @jid = Blather::JID.new('n@d/r')
90 | @i = Blather::RosterItem.new @jid
91 |
92 | @p = Blather::Stanza::Presence::Status.new(:away)
93 | @p.from = 'n@d/a'
94 | @p.priority = 0
95 |
96 | @p2 = Blather::Stanza::Presence::Status.new(:dnd)
97 | @p2.from = 'n@d/b'
98 | @p2.priority = -1
99 |
100 | @p3 = Blather::Stanza::Presence::Status.new(:dnd)
101 | @p3.from = 'n@d/c'
102 | @p3.priority = 10
103 |
104 | @i.status = @p
105 | @i.status = @p2
106 | @i.status = @p3
107 | end
108 |
109 | it 'removes old unavailable presences' do
110 | setup_item_with_presences
111 |
112 | 50.times do |i|
113 | p = Blather::Stanza::Presence::Status.new
114 | p.type = :unavailable
115 | p.from = "n@d/#{i}"
116 | @i.status = p
117 | end
118 |
119 | expect(@i.statuses.size).to eq(4)
120 | end
121 |
122 | it 'initializes groups to [nil] if the item is not part of a group' do
123 | i = Blather::RosterItem.new 'n@d'
124 | expect(i.groups).to eq([nil])
125 | end
126 |
127 | it 'can determine equality' do
128 | item1 = Blather::RosterItem.new 'n@d'
129 | item2 = Blather::RosterItem.new 'n@d'
130 | item1.groups = %w[group1 group2]
131 | item2.groups = %w[group1 group2]
132 | expect(item1 == item2).to eq(true)
133 | end
134 | end
135 |
--------------------------------------------------------------------------------