qswarm 0.0.21 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +264 -1
- data/bin/qswarm +9 -6
- data/lib/qswarm.rb +17 -3
- data/lib/qswarm/agent.rb +160 -40
- data/lib/qswarm/connection.rb +25 -0
- data/lib/qswarm/connections/amqp.rb +167 -0
- data/lib/qswarm/connections/logger.rb +27 -0
- data/lib/qswarm/connections/twitter.rb +148 -0
- data/lib/qswarm/connections/xmpp.rb +92 -0
- data/lib/qswarm/dsl.rb +56 -12
- data/lib/qswarm/swarm.rb +5 -24
- data/lib/qswarm/version.rb +1 -1
- data/qswarm.gemspec +6 -2
- metadata +64 -23
- data/lib/qswarm/broker.rb +0 -118
- data/lib/qswarm/listener.rb +0 -99
- data/lib/qswarm/loggable.rb +0 -15
- data/lib/qswarm/speaker.rb +0 -76
- data/lib/qswarm/speakers/http.rb +0 -67
- data/lib/qswarm/speakers/irc.rb +0 -107
- data/lib/qswarm/speakers/mysql.rb +0 -39
- data/lib/qswarm/speakers/nc.rb +0 -41
@@ -0,0 +1,25 @@
|
|
1
|
+
module Qswarm
|
2
|
+
class Connection
|
3
|
+
attr_reader :format
|
4
|
+
|
5
|
+
def initialize(agent, name, args, &block)
|
6
|
+
@agent = agent
|
7
|
+
@name = name
|
8
|
+
@args = args
|
9
|
+
@on_connect = block_given? ? block : false
|
10
|
+
|
11
|
+
@format = args[:format] || :raw
|
12
|
+
end
|
13
|
+
|
14
|
+
def emit(payload)
|
15
|
+
@agent.emit(@name, :payload => OpenStruct.new(:raw => payload, :format => @format))
|
16
|
+
end
|
17
|
+
|
18
|
+
def sink(args, payload)
|
19
|
+
Qswarm.logger.info ">>> #{payload.raw}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'amqp'
|
2
|
+
require 'cgi'
|
3
|
+
require 'uuid'
|
4
|
+
require 'json'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
module Qswarm
|
8
|
+
module Connections
|
9
|
+
class Amqp < Qswarm::Connection
|
10
|
+
include Qswarm::DSL
|
11
|
+
|
12
|
+
# dsl_accessor :name, :host, :port, :user, :pass, :vhost, :exchange_type, :exchange_name, :durable
|
13
|
+
@@connection = {}
|
14
|
+
|
15
|
+
def initialize(agent, name, args, &block)
|
16
|
+
# Set some defaults
|
17
|
+
@host = 'localhost'
|
18
|
+
@port = 5672
|
19
|
+
@user = 'guest'
|
20
|
+
@pass = 'guest'
|
21
|
+
@vhost = ''
|
22
|
+
@durable = true
|
23
|
+
@prefetch = args[:prefetch] || 0
|
24
|
+
|
25
|
+
decode_uri(args[:uri]) if args[:uri]
|
26
|
+
|
27
|
+
@queues = {}
|
28
|
+
@channels = {}
|
29
|
+
@exchange = nil
|
30
|
+
@instances = nil
|
31
|
+
|
32
|
+
@queue_args = { :auto_delete => true, :durable => true, :exclusive => true }.merge! args[:queue_args] || {}
|
33
|
+
@subscribe_args = { :exclusive => false, :ack => false }.merge! args[:subscribe_args] || {}
|
34
|
+
@bind_args = args[:bind_args] || {}
|
35
|
+
@exchange_type = args[:exchange_type] || :direct
|
36
|
+
@exchange_name = args[:exchange_name] || ''
|
37
|
+
@exchange_args = { :durable => true }.merge! args[:exchange_args] || {}
|
38
|
+
@uuid = UUID.generate if args[:uniq]
|
39
|
+
@bind = args[:bind]
|
40
|
+
|
41
|
+
Signal.trap("INT") do
|
42
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"].close do
|
43
|
+
EM.stop { exit }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def queue(name, routing_key = '', args = nil)
|
51
|
+
@queues["#{name}/#{routing_key}"] ||= begin
|
52
|
+
Qswarm.logger.debug "Binding queue #{name}/#{routing_key}"
|
53
|
+
@queues["#{name}/#{routing_key}"] = channel(name, routing_key).queue(name, args).bind(exchange(channel(name, routing_key)), @bind_args.merge(:routing_key => routing_key))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def exchange(channel = nil)
|
58
|
+
@exchange ||= begin
|
59
|
+
@exchange = AMQP::Exchange.new(channel ||= AMQP::Channel.new(connection, :auto_recovery => true), @exchange_type, @exchange_name, @exchange_args) do |exchange|
|
60
|
+
Qswarm.logger.debug "Declared #{@exchange_type} exchange #{@vhost}/#{@exchange_name}"
|
61
|
+
exchange.on_return do |basic_return, metadata, payload|
|
62
|
+
Qswarm.logger.error "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# ruby-amqp currently limits to 1 consumer per queue (to be fixed in future) so can't pool channels
|
69
|
+
def channel(name, routing_key = '')
|
70
|
+
@channels["#{name}/#{routing_key}"] ||= begin
|
71
|
+
Qswarm.logger.debug "Opening channel for #{name}/#{routing_key}"
|
72
|
+
@channels["#{name}/#{routing_key}"] = AMQP::Channel.new(connection, AMQP::Channel.next_channel_id, :auto_recovery => true, :prefetch => @prefetch) do |c|
|
73
|
+
@channels["#{name}/#{routing_key}"].on_error do |channel, channel_close|
|
74
|
+
Qswarm.logger.error "[channel.close] Reply code = #{channel_close.reply_code}, reply text = #{channel_close.reply_text}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def connection
|
81
|
+
# Pool connections at the class level
|
82
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"] ||= begin
|
83
|
+
Qswarm.logger.debug "Connecting to AMQP broker #{self.to_s}"
|
84
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"] = AMQP.connect(self.to_s, :heartbeat => 30, :on_tcp_connection_failure => Proc.new { |settings|
|
85
|
+
Qswarm.logger.error "AMQP initial connection failure to #{settings[:host]}:#{settings[:port]}/#{settings[:vhost]}"
|
86
|
+
EM.stop
|
87
|
+
}, :on_possible_authentication_failure => Proc.new { |settings|
|
88
|
+
Qswarm.logger.error "AMQP initial authentication failed for #{settings[:host]}:#{settings[:port]}/#{settings[:vhost]}"
|
89
|
+
EM.stop
|
90
|
+
}
|
91
|
+
) do |c|
|
92
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"].on_recovery do |connection|
|
93
|
+
Qswarm.logger.debug "Recovered from AMQP network failure"
|
94
|
+
end
|
95
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"].on_tcp_connection_loss do |connection|
|
96
|
+
# reconnect in 10 seconds
|
97
|
+
Qswarm.logger.error "AMQP TCP connection lost, reconnecting in 2s"
|
98
|
+
connection.periodically_reconnect(2)
|
99
|
+
end
|
100
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"].on_connection_interruption do |connection|
|
101
|
+
Qswarm.logger.error "AMQP connection interruption"
|
102
|
+
end
|
103
|
+
# Force reconnect on heartbeat loss to cope with our funny firewall issues
|
104
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"].on_skipped_heartbeats do |connection, settings|
|
105
|
+
Qswarm.logger.error "Skipped heartbeats detected"
|
106
|
+
end
|
107
|
+
@@connection["#{@host}:#{@port}/#{@vhost}"].on_error do |connection, connection_close|
|
108
|
+
Qswarm.logger.error "AMQP connection has been closed. Reply code = #{connection_close.reply_code}, reply text = #{connection_close.reply_text}"
|
109
|
+
if connection_close.reply_code == 320
|
110
|
+
Qswarm.logger.error "Set a 30s reconnection timer"
|
111
|
+
# every 30 seconds
|
112
|
+
connection.periodically_reconnect(30)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
Qswarm.logger.debug "Connected to AMQP broker #{self.to_s}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def decode_uri(uri)
|
121
|
+
@user, @pass, @host, @port, @vhost = uri.match(/([^:]+):([^@]+)@([^:]+):([^\/]+)\/(.*)/).captures
|
122
|
+
end
|
123
|
+
|
124
|
+
def ack?
|
125
|
+
@subscribe_args[:ack]
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_s
|
129
|
+
"amqp://#{@user}:#{@pass}@#{@host}:#{@port}/#{CGI.escape('/' + @vhost)}"
|
130
|
+
end
|
131
|
+
|
132
|
+
def status
|
133
|
+
"AMQP connection #{@name.inspect} at #{@args[:uri]}, bound to #{@args[:bind]}/#{@args[:bind_args]} on #{@args[:exchange_type].inspect} exchange #{@args[:exchange_name]}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def run
|
137
|
+
if !@bind.nil?
|
138
|
+
[*@bind].each do |bind|
|
139
|
+
queue(@agent.name.to_s + '.' + @name.to_s + @uuid ||= '', bind, @queue_args).subscribe(@subscribe_args) do |metadata, payload|
|
140
|
+
emit metadata, payload
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
dsl_call(&@on_connect) if @on_connect
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def emit(metadata, payload)
|
149
|
+
Qswarm.logger.info "[#{@agent.name.inspect}] :amqp connection #{@name.inspect} bound to #{metadata.routing_key}, received #{payload.inspect}"
|
150
|
+
|
151
|
+
@agent.emit(@name, :payload => OpenStruct.new(:raw => payload, :headers => (metadata.headers.nil? ? {} : Hash[metadata.headers.map{ |k, v| [k.to_sym, v] }]).merge(:routing_key => metadata.routing_key), :format => @format))
|
152
|
+
metadata.ack if ack?
|
153
|
+
end
|
154
|
+
|
155
|
+
def sink(args, payload)
|
156
|
+
[*args[:routing_key]].each do |routing_key|
|
157
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Sinking #{payload.raw.inspect} to AMQP routing_key #{routing_key.inspect}"
|
158
|
+
if args[:headers] || payload.headers
|
159
|
+
exchange.publish payload.raw, :routing_key => routing_key, :headers => (args[:headers] ? args[:headers] : payload.headers).merge(:routing_key => routing_key)
|
160
|
+
else
|
161
|
+
exchange.publish payload.raw, :routing_key => routing_key
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Qswarm
|
2
|
+
module Connections
|
3
|
+
class Logger < Qswarm::Connection
|
4
|
+
include Qswarm::DSL
|
5
|
+
|
6
|
+
attr_reader :format
|
7
|
+
|
8
|
+
def initialize(agent, name, args, &block)
|
9
|
+
@filename = args[:filename] || '/tmp/qswarm-logger.log'
|
10
|
+
|
11
|
+
super(agent, name, args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def emit(payload)
|
15
|
+
end
|
16
|
+
|
17
|
+
def sink(args, payload)
|
18
|
+
@file.puts payload.raw
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
@file = File.open(@filename, 'a')
|
23
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Logging to #{@filename}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'tweetstream'
|
2
|
+
require 'twitter'
|
3
|
+
require 'json'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
module Qswarm
|
7
|
+
module Connections
|
8
|
+
class Twitter < Qswarm::Connection
|
9
|
+
include Qswarm::DSL
|
10
|
+
|
11
|
+
def initialize(agent, name, args, &block)
|
12
|
+
TweetStream.configure do |config|
|
13
|
+
config.consumer_key = args[:consumer_key]
|
14
|
+
config.consumer_secret = args[:consumer_secret]
|
15
|
+
config.oauth_token = args[:oauth_token]
|
16
|
+
config.oauth_token_secret = args[:oauth_token_secret]
|
17
|
+
config.auth_method = :oauth
|
18
|
+
end
|
19
|
+
|
20
|
+
@rest_client = ::Twitter::Client.new(
|
21
|
+
:consumer_key => args[:consumer_key],
|
22
|
+
:consumer_secret => args[:consumer_secret],
|
23
|
+
:oauth_token => args[:oauth_token],
|
24
|
+
:oauth_token_secret => args[:oauth_token_secret]
|
25
|
+
)
|
26
|
+
|
27
|
+
@track = args[:track]
|
28
|
+
@follow = args[:follow]
|
29
|
+
@list = args[:list]
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def emit(payload)
|
35
|
+
@agent.emit(@name, :payload => OpenStruct.new(payload))
|
36
|
+
end
|
37
|
+
|
38
|
+
def sink(metadata, payload)
|
39
|
+
Qswarm.logger.info ">>> #{payload}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def status
|
43
|
+
"Connected to tweetstream, tracking #{@track.to_s}, following #{@follow.to_s}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def run
|
47
|
+
begin
|
48
|
+
if @track
|
49
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Tracking keywords: " + @track.to_s
|
50
|
+
TweetStream::Client.new.track( @track.values.flatten.reject { |k| /^-/.match(k) } ) do |status|
|
51
|
+
@track.each do |group, list|
|
52
|
+
matches = []
|
53
|
+
list.each do |keyword|
|
54
|
+
# Text doesn't include any words in the phrase prefixed with -
|
55
|
+
if keyword.split(' ').select { |k| /^-/.match(k) }.none? { |word| status.text.downcase.include? word[1..-1].downcase }
|
56
|
+
# Text contains all of the words in the phrase
|
57
|
+
if keyword.split(' ').reject { |k| /^-/.match(k) }.all? { |word| status.text.downcase.include? word.downcase }
|
58
|
+
matches << keyword
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if !matches.empty?
|
64
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Sending :track/#{group.inspect} #{status.user.screen_name} :: #{status.text} :: #{matches.to_s}"
|
65
|
+
emit(:raw => status.attrs, :headers => { :type => :track, :group => group, :matches => matches })
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# end.on_limit do |skip_count|
|
69
|
+
# Qswarm.logger.error "[#{@agent.name.inspect} #{@name.inspect}] There were #{skip_count} tweets missed because of rate limiting."
|
70
|
+
end.on_error do |message|
|
71
|
+
Qswarm.logger.error "[#{@agent.name.inspect} #{@name.inspect}] #{message}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if @follow
|
76
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Tracking Users: " + @follow.to_s
|
77
|
+
TweetStream::Client.new.follow( *@follow.values.flatten ) do |status|
|
78
|
+
@follow.each do |group, users|
|
79
|
+
if users.include?(status.user.id)
|
80
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Sending :follow/#{group.inspect} #{status.user.screen_name} :: #{status.text}"
|
81
|
+
emit(:raw => status.attrs, :headers => { :type => :follow, :group => group, :user_id => status.user.id })
|
82
|
+
end
|
83
|
+
end
|
84
|
+
# end.on_limit do |skip_count|
|
85
|
+
# Qswarm.logger.error "[#{@agent.name.inspect} #{@name.inspect}] There were #{skip_count} tweets missed because of rate limiting."
|
86
|
+
end.on_error do |message|
|
87
|
+
Qswarm.logger.error "[#{@agent.name.inspect} #{@name.inspect}] #{message}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
rescue TweetStream::ReconnectError
|
92
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Hit max reconnects, restarting tweetstream in 60 seconds ..."
|
93
|
+
EM.timer(60, run)
|
94
|
+
end
|
95
|
+
|
96
|
+
if @list
|
97
|
+
timer = 30
|
98
|
+
since_id = {}
|
99
|
+
|
100
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Tracking List: " + @list.to_s + " every #{timer} seconds"
|
101
|
+
|
102
|
+
@list.each do |group, lists|
|
103
|
+
lists.each do |user, slug|
|
104
|
+
@rest_client.list_timeline(user, slug).each do |status|
|
105
|
+
since_id["#{user}/#{slug}"] = status.attrs[:id] and break
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
EventMachine::PeriodicTimer.new(timer) do
|
110
|
+
lists.each do |user, slug|
|
111
|
+
begin
|
112
|
+
@rest_client.list_timeline(user, slug, { :since_id => since_id["#{user}/#{slug}"] }).each do |status|
|
113
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Sending :list/#{slug.inspect} #{status.attrs[:user][:screen_name]} :: #{status.text}"
|
114
|
+
emit(:raw => status.attrs, :headers => { :type => :list, :group => group, :user_id => user, :slug => slug })
|
115
|
+
since_id["#{user}/#{slug}"] = status.attrs[:id]
|
116
|
+
end
|
117
|
+
rescue ::Twitter::Error::ClientError
|
118
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Twitter REST API client error"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
dsl_call(&@on_connect) if @on_connect
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
__END__
|
132
|
+
|
133
|
+
end.on_reconnect do |timeout, retries|
|
134
|
+
# puts "RECONNECT #{retries}"
|
135
|
+
end
|
136
|
+
c.on_error do |message|
|
137
|
+
puts "ERROR #{message}"
|
138
|
+
end
|
139
|
+
# client.on_delete do |status_id, user_id|
|
140
|
+
# puts "DELETE #{status_id}"
|
141
|
+
# end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
# c.on_limit do |skip_count|
|
146
|
+
# puts "RATE LIMIT"
|
147
|
+
# end
|
148
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'blather/client/dsl'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Qswarm
|
5
|
+
module Connections
|
6
|
+
class QBlather
|
7
|
+
include Blather::DSL
|
8
|
+
|
9
|
+
Blather.logger.level = Logger::INFO
|
10
|
+
|
11
|
+
def on_connect(block)
|
12
|
+
self.instance_eval(&block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Xmpp < Qswarm::Connection
|
17
|
+
include Qswarm::DSL
|
18
|
+
|
19
|
+
def initialize(agent, name, args, &block)
|
20
|
+
@channels = []
|
21
|
+
@connected = false
|
22
|
+
@connection = nil
|
23
|
+
@real_name = args[:real_name] || 'Bot'
|
24
|
+
|
25
|
+
# Use the block for Blather bot DSL
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def sink(args, payload)
|
30
|
+
if @connected
|
31
|
+
# Use channel jid argument from write or from connection itself
|
32
|
+
channel = args.nil? || args[:channel].nil? ? @args[:channel] : args[:channel]
|
33
|
+
join channel;
|
34
|
+
|
35
|
+
[*channel].each do |c|
|
36
|
+
Qswarm.logger.info "[#{@agent.name.inspect} #{@name.inspect}] Sinking #{payload.raw.inspect} to XMPP channel #{c.inspect}"
|
37
|
+
@connection.say c, payload.raw, :groupchat
|
38
|
+
end
|
39
|
+
else
|
40
|
+
EventMachine::Timer.new(5,self.sink(args, payload))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
xmpp_connect @args[:jid], @args[:password], @args[:channel]
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def join(channel)
|
51
|
+
[*channel].each do |c|
|
52
|
+
next if @channels.include? c
|
53
|
+
Qswarm.logger.debug "Joining XMPP channel #{c}"
|
54
|
+
@connection.join c, @real_name
|
55
|
+
@channels << c
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def xmpp_connect(jid, password, channel)
|
60
|
+
Qswarm.logger.debug "Connecting to XMPP server #{jid}"
|
61
|
+
|
62
|
+
s = QBlather.new
|
63
|
+
@connection = s
|
64
|
+
|
65
|
+
s.setup jid, password
|
66
|
+
|
67
|
+
s.when_ready do
|
68
|
+
Qswarm.logger.debug "Connected to XMPP server #{jid}"
|
69
|
+
@connected = true
|
70
|
+
s.on_connect(@on_connect) if @on_connect
|
71
|
+
join channel unless channel.nil?
|
72
|
+
# Hipchat has a 150s inactivity timer
|
73
|
+
EventMachine::PeriodicTimer.new(60) { s << ' ' }
|
74
|
+
end
|
75
|
+
|
76
|
+
s.disconnected do
|
77
|
+
Qswarm.logger.error "Lost XMPP connection to #{jid}, reconnecting..."
|
78
|
+
@connected = false
|
79
|
+
@connection.run
|
80
|
+
end
|
81
|
+
|
82
|
+
EM.defer do
|
83
|
+
s.run
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def status
|
88
|
+
"XMPP connected to #{@args[:jid]}, present in channels #{@channel.to_s}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|