qswarm 0.0.21 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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