qswarm 0.0.21 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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