weeter 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +9 -0
  4. data/README.md +98 -0
  5. data/Rakefile +2 -0
  6. data/bin/weeter +6 -0
  7. data/bin/weeter_control +7 -0
  8. data/lib/weeter/.DS_Store +0 -0
  9. data/lib/weeter/cli.rb +24 -0
  10. data/lib/weeter/configuration/client_app_config.rb +12 -0
  11. data/lib/weeter/configuration/twitter_config.rb +21 -0
  12. data/lib/weeter/configuration.rb +22 -0
  13. data/lib/weeter/plugins/lib/oauth_http.rb +40 -0
  14. data/lib/weeter/plugins/lib/redis.rb +17 -0
  15. data/lib/weeter/plugins/notification/http.rb +26 -0
  16. data/lib/weeter/plugins/notification/resque.rb +39 -0
  17. data/lib/weeter/plugins/notification_plugin.rb +26 -0
  18. data/lib/weeter/plugins/subscription/http.rb +45 -0
  19. data/lib/weeter/plugins/subscription/redis.rb +46 -0
  20. data/lib/weeter/plugins/subscription_plugin.rb +26 -0
  21. data/lib/weeter/plugins.rb +4 -0
  22. data/lib/weeter/runner.rb +40 -0
  23. data/lib/weeter/tasks.rb +16 -0
  24. data/lib/weeter/twitter/tweet_consumer.rb +67 -0
  25. data/lib/weeter/twitter/tweet_item.rb +35 -0
  26. data/lib/weeter/twitter.rb +2 -0
  27. data/lib/weeter/version.rb +3 -0
  28. data/lib/weeter.rb +29 -0
  29. data/log/.gitignore +0 -0
  30. data/spec/.DS_Store +0 -0
  31. data/spec/spec_helper.rb +11 -0
  32. data/spec/weeter/configuration/client_app_config_spec.rb +18 -0
  33. data/spec/weeter/configuration/twitter_config_spec.rb +39 -0
  34. data/spec/weeter/configuration_spec.rb +15 -0
  35. data/spec/weeter/plugins/notification_plugin_spec.rb +22 -0
  36. data/spec/weeter/plugins/subscription/update_server_spec.rb +30 -0
  37. data/spec/weeter/plugins/subscription_plugin_spec.rb +23 -0
  38. data/spec/weeter/runner_spec.rb +7 -0
  39. data/spec/weeter/twitter/tweet_consumer_spec.rb +64 -0
  40. data/spec/weeter/twitter/tweet_item_spec.rb +66 -0
  41. data/weeter.conf.example +51 -0
  42. data/weeter.gemspec +34 -0
  43. metadata +228 -0
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .rvmrc
2
+ .bundle
3
+ weeter.pid
4
+ weeter.conf
5
+ log/*.log
6
+ vendor/bundle
7
+ .DS_Store
8
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # The gem's dependencies are specified in weeter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+ Portions copyright (c) 2010-2011 Weplay, Inc.
3
+ Portions copyright (c) 2011 Yapp, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ Weeter is a tireless worker who accepts a set of Twitter users to follow and terms to track, subscribes using Twitter's streaming API, and notifies your app with each new tweet (or tweet deletion).
2
+
3
+ Supported strategies for tweet notification include HTTP (issue a POST to your app) and Resque (queue a job). Weeter is extensible with other notification strategies.
4
+
5
+ Status
6
+ ======
7
+ Alpha. A previous version of this code has been in production for some time. It has been substantially refactored and will be battle-tested soon.
8
+
9
+ Getting set up
10
+ ==============
11
+
12
+ $ bundle install
13
+
14
+ Make a copy of the weeter.conf.example file named weeter.conf. Twitter configuration, client app configuration and weeter configuration are defined in separate
15
+ blocks. To configure how you connect to Twitter (basic auth or oauth), modify the twitter section of the configuration.
16
+
17
+ To configure how weeter connects to your client app, modify the client app configuration section:
18
+
19
+ Notifications
20
+ -------------
21
+
22
+ * *notification_plugin*: A symbol matching the underscorized name of the NotificationPlugin subclass to use. Current options are :http and :resque
23
+
24
+ For option :http, also provide the following:
25
+
26
+ * *oauth*: See the conf file for an example
27
+
28
+ * *publish_url*: The URL to which new tweets should be posted. Request will be sent with POST method. Example body:
29
+ `id=1111&twitter_user_id=19466709&text=Wassup`
30
+ * *delete_url*: The URL to which data about deleted tweets should be posted. Request will be sent with DELETE method. Example body:
31
+ `id=1111&twitter_user_id=19466709`
32
+
33
+ For option :resque, provide the following:
34
+
35
+ * *queue*: Name of the queue to add the job to
36
+
37
+ * *redis_uri*: Redis connection string
38
+
39
+ Subscriptions
40
+ -------------
41
+
42
+ * *subscription_plugin*: A symbol matching the underscorized name of the SubscrptionsPlugin subclass to use. Current options are :http and :redis
43
+
44
+ For option :http, also provide the following:
45
+
46
+ * *oauth*: See the conf file for an example
47
+
48
+ * *subscriptions_url*: The URL at which to find JSON describing the Twitter users to follow (maximum 5,000 at the default API access level) and the terms to track (maximum 400 at the default API access level). Example content:
49
+ `{"follow":"19466709", "759251"},{"track":"#lolcats","#bieber"}`
50
+
51
+ * *subscription_updates_port*: The port Weeter should listen on for HTTP connections. If you have changes to your subscriptions data, POST the full JSON to the weeter's root URL. This will trigger weeter to reconnect to Twitter with the updated filters in place.
52
+
53
+ For option :redis, also provide the following:
54
+
55
+ * *subscriptions_key*: The Redis key at which the Weeter can find JSON describing the Twitter users to follow (maximum 5,000 at the default API access level) and the terms to track (maximum 400 at the default API access level). Example content:
56
+ `{"follow":"19466709", "759251"},{"track":"#lolcats","#bieber"}`
57
+
58
+ * *subscriptions_changed_channel*: The Redis publish/subscribe channel to subscribe to in order to be notified that the subscriptions have changed. When your app has an updated set of subscriptions, it should update the _subscriptions_key_ and publish a "CHANGED" message to this channel. Weeter will then retrieve an updated set of subscriptions from Redis and reconnect to twitter.
59
+
60
+ * *redis_uri*: Redis connection string
61
+
62
+ Running weeter
63
+ ==============
64
+
65
+ Weeter can be run using the weeter executable installed by the gem.
66
+
67
+ Running Weeter as a daemon
68
+ --------------------------
69
+ The gem also installs a weeter_control executable that can be used to start Weeter as a daemon
70
+
71
+ $ bin/weeter_control start
72
+
73
+ This starts Weeter as a daemon. For other commands and options, run:
74
+
75
+ $ bin/weeter_control --help
76
+
77
+
78
+
79
+ Running specs
80
+ =============
81
+
82
+ $ bundle exec rspec spec/
83
+
84
+ TODO
85
+ ====
86
+ - Better error reporting
87
+ - Make tweet filtering strategy (re-tweets, replies, etc.) more configurable
88
+ - Add specs for plugins
89
+ - Extract plugins into separate gems
90
+ - integration tests
91
+
92
+ Credits
93
+ ======
94
+ Thanks to Weplay for initial development and open sourcing Weeter. In particular, credit goes to Noah Davis and Joey Aghion. Further development by Luke Melia at Yapp.
95
+
96
+ License
97
+ =======
98
+ Weeter is available under the terms of the MIT License http://www.opensource.org/licenses/mit-license.php
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/weeter ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+ require 'weeter'
5
+
6
+ Weeter::Cli.new(ARGV.clone).run
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+ require 'daemons'
6
+
7
+ Daemons.run(File.join(File.dirname(__FILE__), 'weeter'), :dir_mode => :system)
Binary file
data/lib/weeter/cli.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'optparse'
2
+
3
+ module Weeter
4
+ class Cli
5
+
6
+ def initialize(args)
7
+ @configuration_file = File.join(File.dirname(__FILE__), '..', '..', 'weeter.conf')
8
+ args.options do |opts|
9
+ opts.banner = "Usage: #{$0} [options]"
10
+ opts.on("-c", "--configuration=filename", String,
11
+ "Specifies an executable ruby file containing weeter configuration",
12
+ "Default: weeter.conf") do |val|
13
+ @configuration_file = val
14
+ end
15
+ end.parse!
16
+ end
17
+
18
+ def run
19
+ load @configuration_file
20
+ Weeter::Runner.new(Configuration.instance).start
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ require "singleton"
2
+ require 'hashie'
3
+
4
+ module Weeter
5
+ class Configuration
6
+ class ClientAppConfig < Hashie::Mash
7
+ def subscription_updates_port
8
+ self['subscription_updates_port'] || 7337
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ require "singleton"
2
+
3
+ module Weeter
4
+ class Configuration
5
+ class TwitterConfig
6
+ include Singleton
7
+ attr_accessor :basic_auth, :oauth
8
+
9
+ def auth_options
10
+ if oauth
11
+ {:oauth => oauth}
12
+ else
13
+ username = basic_auth[:username]
14
+ password = basic_auth[:password]
15
+ {:auth => "#{username}:#{password}"}
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ require "singleton"
2
+ require "weeter/configuration/client_app_config"
3
+ require "weeter/configuration/twitter_config"
4
+
5
+ module Weeter
6
+
7
+ class Configuration
8
+ include Singleton
9
+ attr_accessor :log_path
10
+
11
+ def twitter
12
+ yield Configuration::TwitterConfig.instance if block_given?
13
+ Configuration::TwitterConfig.instance
14
+ end
15
+
16
+ def client_app
17
+ @client_app_config ||= Configuration::ClientAppConfig.new
18
+ yield @client_app_config if block_given?
19
+ @client_app_config
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ require 'em-http'
2
+ require 'simple_oauth'
3
+
4
+ module Weeter
5
+ module Plugins
6
+ module Net
7
+ class OauthHttp
8
+ def self.get(config, url, params = {})
9
+ request(config, :get, url, params)
10
+ end
11
+
12
+ def self.put(config, url, params = {})
13
+ request(config, :put, url, params)
14
+ end
15
+
16
+ def self.post(config, url, params = {})
17
+ request(config, :post, url, params)
18
+ end
19
+
20
+ def self.delete(config, url, params = {})
21
+ request(config, :delete, url, params)
22
+ end
23
+
24
+ def self.request(config, method, url, params = {})
25
+ if method == :post
26
+ request_options = {:body => params}
27
+ else
28
+ request_options = {:query => params}
29
+ end
30
+ request_options.merge!(:head => {"Authorization" => oauth_header(config, url, params, method.to_s.upcase)}) if config.oauth
31
+ EM::HttpRequest.new(url).send(method, request_options)
32
+ end
33
+
34
+ def self.oauth_header(config, uri, params, http_method)
35
+ ::SimpleOauth::Header.new(http_method, uri, params, config.oauth).to_s
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ require 'em-hiredis'
2
+
3
+ module Weeter
4
+ module Plugins
5
+ module Net
6
+ module Redis
7
+ def create_redis_client
8
+ @redis ||= begin
9
+ redis = EM::Hiredis.connect(@config.redis_uri)
10
+ redis.callback { Weeter.logger.info "Connected to Redis" }
11
+ redis
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Weeter
2
+ module Plugins
3
+ module Notification
4
+ class Http
5
+ def initialize(client_app_config)
6
+ @config = client_app_config
7
+ end
8
+
9
+ def publish_tweet(tweet_item)
10
+ id = tweet_item['id_str']
11
+ text = tweet_item['text']
12
+ user_id = tweet_item['user']['id_str']
13
+ Weeter.logger.info("Publishing tweet #{id} from user #{user_id}: #{text}")
14
+ Weeter::Plugins::Net::OauthHttp.post(@config, @config.publish_url, {:id => id, :text => text, :twitter_user_id => user_id})
15
+ end
16
+
17
+ def delete_tweet(tweet_item)
18
+ id = tweet_item['delete']['status']['id'].to_s
19
+ user_id = tweet_item['delete']['status']['user_id'].to_s
20
+ Weeter.logger.info("Deleting tweet #{id} for user #{user_id}")
21
+ Weeter::Plugins::Net::OauthHttp.delete(@config, @config.delete_url, {:id => id, :twitter_user_id => user_id})
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ module Weeter
2
+ module Plugins
3
+ module Notification
4
+ class Resque
5
+ include Weeter::Plugins::Net::Redis
6
+
7
+ def initialize(client_app_config)
8
+ @config = client_app_config
9
+ end
10
+
11
+ def publish_tweet(tweet_item)
12
+ resque_job = %Q|{"class":"WeeterPublishTweetJob","args":[#{tweet_item.to_json}]}|
13
+ Weeter.logger.info("Publishing tweet #{tweet_item['id']} from user #{tweet_item['user']['id_str']}: #{tweet_item['text']}")
14
+ enqueue(resque_job)
15
+ end
16
+
17
+ def delete_tweet(tweet_item)
18
+ resque_job = %Q|{"class":"WeeterDeleteTweetJob","args":[#{tweet_item.to_json}]}|
19
+ Weeter.logger.info("Deleting tweet #{tweet_item['id']} for user #{tweet_item['user']['id_str']}")
20
+ enqueue(resque_job)
21
+ end
22
+
23
+ protected
24
+
25
+ def redis
26
+ @redis ||= create_redis_client
27
+ end
28
+
29
+ def enqueue(job)
30
+ redis.rpush(queue_key, job)
31
+ end
32
+
33
+ def queue_key
34
+ "resque:queue:#{@config.queue}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ require 'weeter/plugins/notification/http'
2
+ require 'weeter/plugins/notification/resque'
3
+
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'active_support/core_ext/module/delegation'
6
+
7
+ module Weeter
8
+ module Plugins
9
+ class NotificationPlugin
10
+ delegate :publish_tweet, :to => :configured_plugin
11
+ delegate :delete_tweet, :to => :configured_plugin
12
+
13
+ def initialize(client_app_config)
14
+ @config = client_app_config
15
+ end
16
+
17
+ protected
18
+ def configured_plugin
19
+ @configured_plugin ||= begin
20
+ Weeter.logger.info("Using #{@config.notification_plugin} notification plugin")
21
+ Notification.const_get(@config.notification_plugin.to_s.camelize).new(@config)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,45 @@
1
+ require 'evma_httpserver'
2
+ require 'multi_json'
3
+
4
+ module Weeter
5
+ module Plugins
6
+ module Subscription
7
+ class Http
8
+ def initialize(client_app_config)
9
+ @config = client_app_config
10
+ end
11
+
12
+ def get_initial_filters(&block)
13
+ http = Weeter::Plugins::Net::OauthHttp.get(@config, @config.subscriptions_url)
14
+ http.callback {
15
+ filter_params = {}
16
+ if http.response_header.status == 200
17
+ yield MultiJson.decode(http.response)
18
+ else
19
+ Weeter.logger.error "Initial filters request failed with response code #{http.response_header.status}."
20
+ yield
21
+ end
22
+ }
23
+ end
24
+
25
+ def listen_for_filter_update(tweet_consumer)
26
+ EM.start_server('localhost', @config.subscription_updates_port, UpdateServer) do |conn|
27
+ conn.tweet_consumer = tweet_consumer
28
+ end
29
+ end
30
+
31
+ class UpdateServer < EM::Connection
32
+ include EM::HttpServer
33
+ attr_accessor :tweet_consumer
34
+
35
+ def process_http_request
36
+ Weeter.logger.info("Reconnecting Twitter stream")
37
+ filter_params = MultiJson.decode(@http_post_content)
38
+ tweet_consumer.reconnect(filter_params)
39
+ EM::DelegatedHttpResponse.new(self).send_response
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ require 'multi_json'
2
+
3
+ module Weeter
4
+ module Plugins
5
+ module Subscription
6
+ class Redis
7
+ include Weeter::Plugins::Net::Redis
8
+
9
+ def initialize(client_app_config)
10
+ @config = client_app_config
11
+ end
12
+
13
+ def get_initial_filters(&block)
14
+ redis.get(@config.subscriptions_key) do |value|
15
+ if value.nil?
16
+ raise "Expected to find subscription data at redis key #{@config.subscriptions_key}"
17
+ end
18
+ yield MultiJson.decode(value)
19
+ end
20
+ end
21
+
22
+ def listen_for_filter_update(tweet_consumer)
23
+ pub_sub_redis.subscribe(@config.subscriptions_changed_channel)
24
+ pub_sub_redis.on(:message) do |channel, message|
25
+ Weeter.logger.info [:message, channel, message]
26
+ Weeter.logger.info("Reconnecting Twitter stream")
27
+ get_initial_filters do |filter_params|
28
+ tweet_consumer.reconnect(filter_params)
29
+ end
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ def redis
36
+ @redis ||= create_redis_client
37
+ end
38
+
39
+ def pub_sub_redis
40
+ @pub_sub_redis ||= create_redis_client
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ require 'weeter/plugins/subscription/http'
2
+ require 'weeter/plugins/subscription/redis'
3
+
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'active_support/core_ext/module/delegation'
6
+
7
+ module Weeter
8
+ module Plugins
9
+ class SubscriptionPlugin
10
+ delegate :get_initial_filters, :to => :configured_plugin
11
+ delegate :listen_for_filter_update, :to => :configured_plugin
12
+
13
+ def initialize(client_app_config)
14
+ @config = client_app_config
15
+ end
16
+
17
+ protected
18
+ def configured_plugin
19
+ @configured_plugin ||= begin
20
+ Weeter.logger.info("Using #{@config.subscription_plugin} subscription plugin")
21
+ Subscription.const_get(@config.subscription_plugin.to_s.camelize).new(@config)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ require 'weeter/plugins/lib/oauth_http'
2
+ require 'weeter/plugins/lib/redis'
3
+ require 'weeter/plugins/notification_plugin'
4
+ require 'weeter/plugins/subscription_plugin'
@@ -0,0 +1,40 @@
1
+ require 'em-http'
2
+
3
+ module Weeter
4
+ class Runner
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ Weeter.logger.info("Starting weeter with configuration: #{@config.inspect}")
9
+ end
10
+
11
+ def start
12
+ EM.run {
13
+ subscription_plugin.get_initial_filters do |filter_params|
14
+ Weeter.logger.info("Connecting to twitter with initial filters")
15
+ tweet_consumer.connect(filter_params)
16
+ subscription_plugin.listen_for_filter_update(tweet_consumer)
17
+
18
+ trap('TERM') do
19
+ Weeter.logger.info("Stopping weeter")
20
+ EM.stop if EM.reactor_running?
21
+ end
22
+ end
23
+ }
24
+ end
25
+
26
+ protected
27
+
28
+ def notification_plugin
29
+ @notification_plugin ||= Weeter::Plugins::NotificationPlugin.new(@config.client_app)
30
+ end
31
+
32
+ def subscription_plugin
33
+ @subscription_plugin ||= Weeter::Plugins::SubscriptionPlugin.new(@config.client_app)
34
+ end
35
+
36
+ def tweet_consumer
37
+ @tweet_consumer ||= Weeter::Twitter::TweetConsumer.new(@config.twitter, notification_plugin)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ # require 'weeter/tasks'
2
+ # will give you the weeter tasks
3
+
4
+ namespace :weeter do
5
+ task :setup do
6
+ require 'weeter'
7
+ # extend this task to set the config path
8
+ end
9
+
10
+ desc "Start Weeter"
11
+ task :start => :setup do
12
+ configuration_file = ENV['WEETER_CONFIG_PATH']
13
+ load configuration_file
14
+ Weeter::Runner.new(Weeter::Configuration.instance).start
15
+ end
16
+ end
@@ -0,0 +1,67 @@
1
+ require 'twitter/json_stream'
2
+ require 'multi_json'
3
+
4
+ module Weeter
5
+ module Twitter
6
+ class TweetConsumer
7
+
8
+ def initialize(twitter_config, notifier)
9
+ @config = twitter_config
10
+ @notifier = notifier
11
+ end
12
+
13
+ def connect(filter_params)
14
+ filter_params = clean_filter_params(filter_params)
15
+ connect_options = {:ssl => true, :params => filter_params, :method => 'POST'}.merge(@config.auth_options)
16
+ @stream = ::Twitter::JSONStream.connect(connect_options)
17
+
18
+ @stream.each_item do |item|
19
+ begin
20
+ tweet_item = TweetItem.new(MultiJson.decode(item))
21
+
22
+ if tweet_item.deletion?
23
+ @notifier.delete_tweet(tweet_item)
24
+ elsif tweet_item.publishable?
25
+ @notifier.publish_tweet(tweet_item)
26
+ else
27
+ ignore_tweet(tweet_item)
28
+ end
29
+ rescue => ex
30
+ Weeter.logger.error("Twitter stream tweet exception: #{ex.class.name}: #{ex.message}")
31
+ end
32
+ end
33
+
34
+ @stream.on_error do |msg|
35
+ Weeter.logger.error("Twitter stream error: #{msg}. Connect options were #{connect_options.inspect}")
36
+ end
37
+
38
+ @stream.on_max_reconnects do |timeout, retries|
39
+ Weeter.logger.error("Twitter stream max-reconnects reached: timeout=#{timeout}, retries=#{retries}")
40
+ end
41
+ end
42
+
43
+ def reconnect(filter_params)
44
+ @stream.stop
45
+ connect(filter_params)
46
+ end
47
+
48
+ protected
49
+
50
+ def clean_filter_params(p)
51
+ return {} if p.nil?
52
+ cleaned_params = {}
53
+ cleaned_params['follow'] = p['follow'] if (p['follow'] || []).any?
54
+ cleaned_params['track'] = p['track'] if (p['track'] || []).any?
55
+ cleaned_params
56
+ end
57
+
58
+ def ignore_tweet(tweet_item)
59
+ id = tweet_item['id_str']
60
+ text = tweet_item['text']
61
+ user_id = tweet_item['user']['id_str']
62
+ Weeter.logger.info("Ignoring tweet #{id} from user #{user_id}: #{text}")
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,35 @@
1
+ require 'multi_json'
2
+
3
+ module Weeter
4
+
5
+ class TweetItem
6
+ def initialize(tweet_hash)
7
+ @tweet_hash = tweet_hash
8
+ end
9
+
10
+ def deletion?
11
+ !@tweet_hash['delete'].nil?
12
+ end
13
+
14
+ def retweeted?
15
+ !@tweet_hash['retweeted_status'].nil? || @tweet_hash['text'] =~ /^RT @/i
16
+ end
17
+
18
+ def reply?
19
+ !@tweet_hash['in_reply_to_user_id_str'].nil? || @tweet_hash['text'] =~ /^@/
20
+ end
21
+
22
+ def publishable?
23
+ !retweeted? && !reply?
24
+ end
25
+
26
+ def [](val)
27
+ @tweet_hash[val]
28
+ end
29
+
30
+ def to_json
31
+ MultiJson.encode(@tweet_hash)
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,2 @@
1
+ require 'weeter/twitter/tweet_consumer'
2
+ require 'weeter/twitter/tweet_item'