weeter 0.11.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,8 +1,10 @@
1
1
  .rvmrc
2
2
  .bundle
3
+ .rspec
3
4
  weeter.pid
4
5
  weeter.conf
5
6
  log/*.log
6
7
  vendor/bundle
7
8
  .DS_Store
8
- Gemfile.lock
9
+ Gemfile.lock
10
+ pkg
data/.travis.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  language: ruby
2
2
  bundler_args: --without debug
3
3
  rvm:
4
- - 1.9.2
4
+ - 1.9.3
5
5
  script: bundle exec rspec spec
@@ -2,10 +2,15 @@ require 'active_support/core_ext/numeric/time'
2
2
 
3
3
  module Weeter
4
4
  class Limitator
5
+ Result = Struct.new(:status, :limited_keys)
6
+
7
+ DO_NOT_LIMIT = :do_not_limit
8
+ INITIATE_LIMITING = :initiate_limiting
9
+ CONTINUE_LIMITING = :continue_limiting
5
10
 
6
11
  module UNLIMITED
7
- def self.limit?(*args)
8
- false
12
+ def self.limit_status(*args)
13
+ DO_NOT_LIMIT
9
14
  end
10
15
  end
11
16
 
@@ -27,7 +32,7 @@ module Weeter
27
32
  attr_accessor :lookup, :window, :max
28
33
 
29
34
  def initialize(options = {})
30
- self.window= TimeWindow.new({
35
+ self.window = TimeWindow.new({
31
36
  start: self.now,
32
37
  duration: options.fetch(:duration)
33
38
  })
@@ -37,14 +42,26 @@ module Weeter
37
42
  flush
38
43
  end
39
44
 
40
- def limit?(*keys)
45
+ def process(*keys)
41
46
  ensure_correct_window
42
47
 
43
48
  keys.each do |key|
44
49
  increment(key)
45
50
  end
46
51
 
47
- keys.any? { |key| exceeds_max?(key) }
52
+ result = Result.new
53
+ limited_keys = keys.select { |key| exceeds_max?(key) }
54
+ if limited_keys.any?
55
+ result[:limited_keys] = limited_keys
56
+ if limited_keys.any? { |key| exceeds_max_by_one?(key) }
57
+ result[:status] = INITIATE_LIMITING
58
+ else
59
+ result[:status] = CONTINUE_LIMITING
60
+ end
61
+ else
62
+ result[:status] = DO_NOT_LIMIT
63
+ end
64
+ result
48
65
  end
49
66
 
50
67
  protected
@@ -61,6 +78,10 @@ module Weeter
61
78
  lookup[key] > max
62
79
  end
63
80
 
81
+ def exceeds_max_by_one?(key)
82
+ lookup[key] == max + 1
83
+ end
84
+
64
85
  def ensure_correct_window
65
86
  return unless window.over?(now)
66
87
 
@@ -5,13 +5,12 @@ module Weeter
5
5
  module Net
6
6
  module Redis
7
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
8
+ redis = EM::Hiredis.connect(@config.redis_uri)
9
+ redis.callback { Weeter.logger.info "Connected to Redis" }
10
+ redis.errback { |message| Weeter.logger.err "Failed to connect to Redis: #{message}" }
11
+ redis
13
12
  end
14
13
  end
15
14
  end
16
15
  end
17
- end
16
+ end
@@ -20,7 +20,15 @@ module Weeter
20
20
  Weeter.logger.info("Deleting tweet #{id} for user #{user_id}")
21
21
  Weeter::Plugins::Net::OauthHttp.delete(@config, @config.delete_url, {:id => id, :twitter_user_id => user_id})
22
22
  end
23
+
24
+ def notify_missed_tweets(tweet_item)
25
+ Weeter.logger.info("Weeter was limited by Twitter. #{tweet_item.missed_tweets_count} tweets missed.")
26
+ end
27
+
28
+ def notify_rate_limiting_initiated(tweet_item, limited_keys)
29
+ Weeter.logger.info("Initiated rate limiting with tweet: #{tweet_item.to_json}")
30
+ end
23
31
  end
24
32
  end
25
33
  end
26
- end
34
+ end
@@ -3,7 +3,7 @@ module Weeter
3
3
  module Notification
4
4
  class Resque
5
5
  include Weeter::Plugins::Net::Redis
6
-
6
+
7
7
  def initialize(client_app_config)
8
8
  @config = client_app_config
9
9
  end
@@ -20,12 +20,26 @@ module Weeter
20
20
  enqueue(resque_job)
21
21
  end
22
22
 
23
+ def notify_missed_tweets(tweet_item)
24
+ resque_job = %Q|{"class":"WeeterMissedTweetsJob","args":[#{tweet_item.to_json}]}|
25
+ Weeter.logger.info("Notifying of missed tweets (#{tweet_item.missed_tweets_count}).")
26
+ enqueue(resque_job)
27
+ end
28
+
29
+ def notify_rate_limiting_initiated(tweet_item, limited_keys)
30
+ payload = tweet_item.to_hash.merge(:limited_keys => limited_keys)
31
+ payload_json = MultiJson.encode(payload)
32
+ resque_job = %Q|{"class":"WeeterRateLimitingInitiatedJob","args":[#{payload_json}]}|
33
+ Weeter.logger.info("Initiated rate limiting with tweet: #{payload_json}")
34
+ enqueue(resque_job)
35
+ end
36
+
23
37
  protected
24
-
38
+
25
39
  def redis
26
40
  @redis ||= create_redis_client
27
41
  end
28
-
42
+
29
43
  def enqueue(job)
30
44
  redis.rpush(queue_key, job)
31
45
  end
@@ -36,4 +50,4 @@ module Weeter
36
50
  end
37
51
  end
38
52
  end
39
- end
53
+ end
@@ -7,13 +7,16 @@ require 'active_support/core_ext/module/delegation'
7
7
  module Weeter
8
8
  module Plugins
9
9
  class NotificationPlugin
10
- delegate :publish_tweet, :to => :configured_plugin
11
- delegate :delete_tweet, :to => :configured_plugin
12
-
10
+ delegate :publish_tweet,
11
+ :delete_tweet,
12
+ :notify_rate_limiting_initiated,
13
+ :notify_missed_tweets,
14
+ :to => :configured_plugin
15
+
13
16
  def initialize(client_app_config)
14
17
  @config = client_app_config
15
18
  end
16
-
19
+
17
20
  protected
18
21
  def configured_plugin
19
22
  @configured_plugin ||= begin
@@ -23,4 +26,4 @@ module Weeter
23
26
  end
24
27
  end
25
28
  end
26
- end
29
+ end
@@ -11,20 +11,24 @@ module Weeter
11
11
  end
12
12
 
13
13
  def get_initial_filters(&block)
14
- redis.get(@config.subscriptions_key) do |value|
14
+ deferred_get = redis.get(@config.subscriptions_key) do |value|
15
15
  if value.nil?
16
16
  raise "Expected to find subscription data at redis key #{@config.subscriptions_key}"
17
17
  end
18
18
  yield MultiJson.decode(value)
19
19
  end
20
+ deferred_get.errback do |message|
21
+ Weeter.logger.error(message)
22
+ end
20
23
  end
21
24
 
22
25
  def listen_for_filter_update(tweet_consumer)
23
26
  pub_sub_redis.subscribe(@config.subscriptions_changed_channel)
24
27
  pub_sub_redis.on(:message) do |channel, message|
25
28
  Weeter.logger.info [:message, channel, message]
26
- Weeter.logger.info("Reconnecting Twitter stream")
29
+ Weeter.logger.info("Retrieving updated filters from redis")
27
30
  get_initial_filters do |filter_params|
31
+ Weeter.logger.info("Triggering reconnect Twitter stream with new filters")
28
32
  tweet_consumer.reconnect(filter_params)
29
33
  end
30
34
  end
@@ -4,8 +4,13 @@ require 'multi_json'
4
4
  module Weeter
5
5
  module Twitter
6
6
  class TweetConsumer
7
+ extend ::Forwardable
7
8
 
8
- attr_reader :limiter
9
+ attr_reader :limiter, :notifier
10
+ def_delegators :@notifier, :notify_missed_tweets,
11
+ :notify_rate_limiting_initiated,
12
+ :delete_tweet,
13
+ :publish_tweet
9
14
 
10
15
  def initialize(twitter_config, notifier, limiter, subscriptions_limit = nil)
11
16
  @config = twitter_config
@@ -18,31 +23,31 @@ module Weeter
18
23
  filter_params = limit_filter_params(filter_params) if @subscriptions_limit
19
24
  filter_params = clean_filter_params(filter_params)
20
25
 
26
+
21
27
  connect_options = {
22
28
  ssl: true,
23
29
  params: filter_params,
24
30
  method: 'POST'
25
31
  }.merge(@config.auth_options)
26
32
 
33
+ Weeter.logger.info("Connecting to Twitter stream...")
27
34
  @stream = ::Twitter::JSONStream.connect(connect_options)
28
35
 
29
36
  @stream.each_item do |item|
30
37
  begin
31
38
  tweet_item = TweetItem.new(MultiJson.decode(item))
32
39
 
33
- if tweet_item.deletion?
34
- @notifier.delete_tweet(tweet_item)
40
+ if tweet_item.limit_notice?
41
+ notify_missed_tweets(tweet_item)
42
+ elsif tweet_item.deletion?
43
+ delete_tweet(tweet_item)
35
44
  elsif tweet_item.publishable?
36
- if limiter.limit?(*tweet_item.limiting_facets)
37
- rate_limit_tweet(tweet_item)
38
- else
39
- @notifier.publish_tweet(tweet_item)
40
- end
45
+ publish_or_rate_limit(tweet_item)
41
46
  else
42
47
  ignore_tweet(tweet_item)
43
48
  end
44
49
  rescue => ex
45
- Weeter.logger.error("Twitter stream tweet exception: #{ex.class.name}: #{ex.message}")
50
+ Weeter.logger.error("Twitter stream tweet exception: #{ex.class.name}: #{ex.message} #{tweet_item.to_json}")
46
51
  end
47
52
  end
48
53
 
@@ -57,6 +62,7 @@ module Weeter
57
62
 
58
63
  def reconnect(filter_params)
59
64
  @stream.stop
65
+ @stream.unbind
60
66
  connect(filter_params)
61
67
  end
62
68
 
@@ -97,15 +103,17 @@ module Weeter
97
103
  return {} if p.nil?
98
104
  cleaned_params = {}
99
105
  cleaned_params['follow'] = p['follow'] if (p['follow'] || []).any?
100
- cleaned_params['follow'] = cleaned_params['follow'].map(&:to_i)
106
+ cleaned_params['follow'] = cleaned_params['follow'].map(&:to_i) if cleaned_params['follow']
101
107
  cleaned_params['track'] = p['track'] if (p['track'] || []).any?
102
108
  cleaned_params
103
109
  end
104
110
 
105
111
  def ignore_tweet(tweet_item)
112
+ return if tweet_item.disconnect_notice?
106
113
  id = tweet_item['id_str']
107
114
  text = tweet_item['text']
108
- user_id = tweet_item['user']['id_str']
115
+ user = tweet_item['user']
116
+ user_id = user['id_str'] if user
109
117
  Weeter.logger.info("Ignoring tweet #{id} from user #{user_id}: #{text}")
110
118
  end
111
119
 
@@ -116,6 +124,19 @@ module Weeter
116
124
 
117
125
  Weeter.logger.info("Rate Limiting tweet #{id} from user #{user_id}: #{text}")
118
126
  end
127
+
128
+ def publish_or_rate_limit(tweet_item)
129
+ limit_result = limiter.process(*tweet_item.limiting_facets)
130
+ case limit_result.status
131
+ when Weeter::Limitator::INITIATE_LIMITING
132
+ notify_rate_limiting_initiated(tweet_item, limit_result.limited_keys)
133
+ rate_limit_tweet(tweet_item)
134
+ when Weeter::Limitator::CONTINUE_LIMITING
135
+ rate_limit_tweet(tweet_item)
136
+ when Weeter::Limitator::DO_NOT_LIMIT
137
+ publish_tweet(tweet_item)
138
+ end
139
+ end
119
140
  end
120
141
  end
121
142
  end
@@ -20,7 +20,20 @@ module Weeter
20
20
  end
21
21
 
22
22
  def publishable?
23
- !retweeted? && !reply?
23
+ !retweeted? && !reply? && !disconnect_notice? && !limit_notice?
24
+ end
25
+
26
+ def disconnect_notice?
27
+ !@tweet_hash['disconnect'].nil?
28
+ end
29
+
30
+ def limit_notice?
31
+ !@tweet_hash['limit'].nil?
32
+ end
33
+
34
+ def missed_tweets_count
35
+ return nil unless limit_notice?
36
+ @tweet_hash['limit']['track']
24
37
  end
25
38
 
26
39
  def [](val)
@@ -31,6 +44,10 @@ module Weeter
31
44
  MultiJson.encode(@tweet_hash)
32
45
  end
33
46
 
47
+ def to_hash
48
+ @tweet_hash
49
+ end
50
+
34
51
  def limiting_facets
35
52
  self['entities']['hashtags'].map do |tag|
36
53
  tag['text'].downcase.chomp
@@ -1,3 +1,3 @@
1
1
  module Weeter
2
- VERSION = "0.11.0"
2
+ VERSION = "0.13.0"
3
3
  end
@@ -17,46 +17,57 @@ describe Weeter::Limitator do
17
17
  it { limitator.should be }
18
18
  end
19
19
 
20
- describe '#limit?' do
20
+ describe '#limit_status' do
21
21
 
22
22
  subject do
23
- limitator.limit?(*keys)
23
+ limitator.process(*keys)
24
24
  end
25
25
 
26
26
  context 'max: 0' do
27
27
  let(:max) { 0 }
28
- it { should be_true }
28
+ its(:status) { should == Weeter::Limitator::INITIATE_LIMITING }
29
+ its(:limited_keys) { should == keys }
29
30
 
30
31
  context 'no keys' do
31
32
  let(:keys) { [] }
32
- it { should be_false }
33
+ its(:status) { should == Weeter::Limitator::DO_NOT_LIMIT }
34
+ its(:limited_keys) { should == nil }
35
+ end
36
+
37
+ context 'two keys' do
38
+ let(:keys) { ['key', 'key2'] }
39
+ its(:status) { should == Weeter::Limitator::INITIATE_LIMITING }
40
+ its(:limited_keys) { should == keys }
33
41
  end
34
42
  end
35
43
 
36
44
  context 'max: 1' do
37
45
  let(:max) { 1 }
38
46
 
39
- it { should be_false }
47
+ its(:status) { should == Weeter::Limitator::DO_NOT_LIMIT }
48
+ its(:limited_keys) { should == nil }
40
49
 
41
50
  context 'two keys within max' do
42
51
  let(:keys) { ['key', 'key2'] }
43
52
 
44
- it { should be_false }
53
+ its(:status) { should == Weeter::Limitator::DO_NOT_LIMIT }
45
54
  end
46
55
 
47
56
  context 'no keys' do
48
57
  let(:keys) { [] }
49
- it { should be_false }
58
+ its(:status) { should == Weeter::Limitator::DO_NOT_LIMIT }
59
+ its(:limited_keys) { should == nil }
50
60
  end
51
61
 
52
- context 'one key outside max' do
62
+ context 'one key just outside max' do
53
63
  before do
54
64
  max.times do
55
- limitator.limit?(*keys)
65
+ limitator.process(*keys)
56
66
  end
57
67
  end
58
68
 
59
- it { should be_true }
69
+ its(:status) { should == Weeter::Limitator::INITIATE_LIMITING }
70
+ its(:limited_keys) { should == keys }
60
71
 
61
72
  context 'outside duration' do
62
73
  let(:some_time_after_duration) do
@@ -67,31 +78,71 @@ describe Weeter::Limitator do
67
78
  limitator.stub(:now).and_return(some_time_after_duration)
68
79
  end
69
80
 
70
- it { should be_false }
81
+ its(:status) { should == Weeter::Limitator::DO_NOT_LIMIT }
82
+ its(:limited_keys) { should == nil }
83
+ end
84
+ end
85
+
86
+ context 'two keys just past max' do
87
+ let(:keys) { ['key', 'key2'] }
88
+
89
+ before do
90
+ limitator.process(*keys)
71
91
  end
92
+
93
+ its(:status) { should == Weeter::Limitator::INITIATE_LIMITING }
94
+ its(:limited_keys) { should == keys }
72
95
  end
73
96
 
74
- context 'two keys outside' do
97
+ context 'two keys past max' do
75
98
  let(:keys) { ['key', 'key2'] }
76
99
 
77
100
  before do
78
- limitator.limit?(*keys)
101
+ limitator.process(*keys)
102
+ limitator.process(*keys)
79
103
  end
80
104
 
81
- it { should be_true }
105
+ its(:status) { should == Weeter::Limitator::CONTINUE_LIMITING }
106
+ its(:limited_keys) { should == keys }
82
107
  end
83
108
 
84
- context 'one key outside max: 1, one key within max: 1' do
109
+ context 'one key just past max: 1, one key within max: 1' do
85
110
  let(:max) { 1 }
86
111
  let(:keys) { ['key', 'key2'] }
87
112
 
88
113
  before do
89
- limitator.limit?(*[keys.first])
114
+ limitator.process(keys.first)
90
115
  end
91
116
 
92
- it { should be_true }
117
+ its(:status) { should == Weeter::Limitator::INITIATE_LIMITING }
118
+ its(:limited_keys) { should == [keys.first] }
119
+ end
120
+
121
+ context 'one key past max: 1, one key within max: 1' do
122
+ let(:max) { 1 }
123
+ let(:keys) { ['key', 'key2'] }
124
+
125
+ before do
126
+ limitator.process(keys.first)
127
+ limitator.process(keys.first)
128
+ end
129
+
130
+ its(:status) { should == Weeter::Limitator::CONTINUE_LIMITING }
131
+ its(:limited_keys) { should == [keys.first] }
132
+ end
133
+
134
+ context 'one key past max: 1, one key just past max: 1' do
135
+ let(:max) { 1 }
136
+ let(:keys) { ['key', 'key2'] }
137
+
138
+ before do
139
+ limitator.process(*[keys.first])
140
+ limitator.process(*[keys.first, keys.last])
141
+ end
142
+
143
+ its(:status) { should == Weeter::Limitator::INITIATE_LIMITING }
144
+ its(:limited_keys) { should == keys }
93
145
  end
94
146
  end
95
147
  end
96
148
  end
97
-
@@ -10,7 +10,7 @@ describe Weeter::Twitter::TweetConsumer do
10
10
 
11
11
  describe "auth" do
12
12
  it 'should use connect to JSON stream with auth options for the configuration' do
13
- @mock_stream = mock('JSONStream', :each_item => nil, :on_error => nil, :on_max_reconnects => nil)
13
+ @mock_stream = mock('JSONStream', :each_item => nil, :on_error => nil, :on_max_reconnects => nil, :on_close => nil)
14
14
  Twitter::JSONStream.stub!(:connect).and_return(@mock_stream)
15
15
 
16
16
  Weeter::Configuration::TwitterConfig.instance.stub!(:auth_options).and_return(:foo => :bar)
@@ -91,13 +91,22 @@ describe Weeter::Twitter::TweetConsumer do
91
91
 
92
92
  describe "connecting to twitter" do
93
93
 
94
+ let(:tweet_values) {
95
+ [@tweet_hash]
96
+ }
97
+ let(:mock_stream) {
98
+ mock_stream = mock('JSONStream', :on_error => nil, :on_max_reconnects => nil, :on_close => nil)
99
+ each_item_stub = mock_stream.stub!(:each_item)
100
+ tweet_values.each do |t|
101
+ each_item_stub.and_yield(MultiJson.encode(t))
102
+ end
103
+ mock_stream
104
+ }
94
105
  before(:each) do
95
106
  @filter_params = {'follow' => ['1','2','3']}
96
107
  Weeter::Configuration::TwitterConfig.instance.stub!(:auth_options).and_return(:foo => :bar)
97
- @tweet_values = {'text' => "Hey", 'id_str' => "123", 'user' => {'id_str' => "1"}}
98
- @mock_stream = mock('JSONStream', :on_error => nil, :on_max_reconnects => nil)
99
- @mock_stream.stub!(:each_item).and_yield(MultiJson.encode(@tweet_values))
100
- Twitter::JSONStream.stub!(:connect).and_return(@mock_stream)
108
+ @tweet_hash = {'text' => "Hey", 'id_str' => "123", 'user' => {'id_str' => "1"}}
109
+ Twitter::JSONStream.stub!(:connect).and_return(mock_stream)
101
110
  @client_proxy = mock('NotificationPlugin', :publish_tweet => nil)
102
111
  @consumer = Weeter::Twitter::TweetConsumer.new(Weeter::Configuration::TwitterConfig.instance, @client_proxy, limiter)
103
112
  end
@@ -107,7 +116,7 @@ describe Weeter::Twitter::TweetConsumer do
107
116
  end
108
117
 
109
118
  it "should instantiate a TweetItem" do
110
- tweet_item = Weeter::TweetItem.new(@tweet_values)
119
+ tweet_item = Weeter::TweetItem.new(@tweet_hash)
111
120
  Weeter::TweetItem.should_receive(:new).with({'text' => "Hey", 'id_str' => "123", 'user' => {'id_str' => "1"}}).and_return(tweet_item)
112
121
  end
113
122
 
@@ -117,22 +126,41 @@ describe Weeter::Twitter::TweetConsumer do
117
126
  end
118
127
 
119
128
  it "should publish new tweet if publishable" do
120
- mock_tweet = mock('tweet', :deletion? => false, :publishable? => true, :limiting_facets => [])
121
- tweet_item = Weeter::TweetItem.stub!(:new).and_return mock_tweet
129
+ mock_tweet = mock('tweet', :deletion? => false, :publishable? => true, :limit_notice? => false, :limiting_facets => [])
130
+ Weeter::TweetItem.stub!(:new).and_return(mock_tweet)
122
131
  @client_proxy.should_receive(:publish_tweet).with(mock_tweet)
123
132
  end
124
133
 
125
134
  it "should not publish unpublishable tweets" do
126
- mock_tweet = mock('tweet', :deletion? => false, :publishable? => false, :[] => '', :limiting_facets => [])
127
- tweet_item = Weeter::TweetItem.stub!(:new).and_return mock_tweet
135
+ mock_tweet = mock('tweet', :deletion? => false, :publishable? => false, :limit_notice? => false, :[] => '', :limiting_facets => [])
136
+ Weeter::TweetItem.stub!(:new).and_return mock_tweet
128
137
  @client_proxy.should_not_receive(:publish_tweet).with(mock_tweet)
129
138
  end
130
139
 
131
140
  it "should delete deletion tweets" do
132
- mock_tweet = mock('tweet', :deletion? => true, :publishable? => false, :limiting_facets => [])
133
- tweet_item = Weeter::TweetItem.stub!(:new).and_return mock_tweet
141
+ mock_tweet = mock('tweet', :deletion? => true, :publishable? => false, :limit_notice? => false, :limiting_facets => [])
142
+ Weeter::TweetItem.stub!(:new).and_return mock_tweet
134
143
  @client_proxy.should_receive(:delete_tweet).with(mock_tweet)
135
144
  end
145
+
146
+ it "should notify when stream is limited by Twitter" do
147
+ tweet_item = Weeter::TweetItem.new({'limit' => { 'track' => 65 } })
148
+ Weeter::TweetItem.stub!(:new).and_return(tweet_item)
149
+ @client_proxy.should_receive(:notify_missed_tweets).with(tweet_item)
150
+ end
151
+
152
+ context "when weeter is initiating rate-limiting on a facet" do
153
+ let(:tweet_values) {
154
+ [@tweet_hash, @tweet_hash]
155
+ }
156
+ it "should notify that rate limiting is being initiated" do
157
+ tweet_item1 = mock('tweet', :deletion? => false, :publishable? => true, :limit_notice? => false, :limiting_facets => ['key'], :[] => '1')
158
+ tweet_item2 = mock('tweet', :deletion? => false, :publishable? => true, :limit_notice? => false, :limiting_facets => ['key'], :[] => '2')
159
+ Weeter::TweetItem.stub!(:new).and_return(tweet_item1, tweet_item2)
160
+
161
+ @client_proxy.should_receive(:notify_rate_limiting_initiated).with(tweet_item2, ['key'])
162
+ end
163
+ end
136
164
  end
137
165
 
138
166
  end
@@ -1,6 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Weeter::TweetItem do
4
+ let(:tweet_json) {
5
+ {'text' => "Hey", 'id_str' => "123", 'user' => {'id_str' => '1'}}
6
+ }
4
7
 
5
8
  describe "deletion?" do
6
9
  it "should be true if it is a deletion request" do
@@ -16,37 +19,54 @@ describe Weeter::TweetItem do
16
19
 
17
20
  describe "publishable" do
18
21
 
19
- before do
20
- @tweet_json = {'text' => "Hey", 'id_str' => "123", 'user' => {'id_str' => '1'}}
21
- end
22
22
 
23
23
  it "should be publishable if not a reply or a retweet" do
24
- item = Weeter::TweetItem.new(@tweet_json)
24
+ item = Weeter::TweetItem.new(tweet_json)
25
25
  item.should be_publishable
26
26
  end
27
27
 
28
28
  it "should not be publishable if implicitly retweeted" do
29
- item = Weeter::TweetItem.new(@tweet_json.merge({'text' => 'RT @joe Hey'}))
29
+ item = Weeter::TweetItem.new(tweet_json.merge({'text' => 'RT @joe Hey'}))
30
30
  item.should_not be_publishable
31
31
  end
32
32
 
33
33
  it "should not be publishable if explicitly retweeted" do
34
- item = Weeter::TweetItem.new(@tweet_json.merge('retweeted_status' => {'id_str' => '111', 'text' => 'Hey', 'user' => {'id_str' => "1"}}))
34
+ item = Weeter::TweetItem.new(tweet_json.merge('retweeted_status' => {'id_str' => '111', 'text' => 'Hey', 'user' => {'id_str' => "1"}}))
35
35
  item.should_not be_publishable
36
36
  end
37
37
 
38
38
  it "should not be publishable if implicit reply" do
39
- item = Weeter::TweetItem.new(@tweet_json.merge('text' => '@joe Hey'))
39
+ item = Weeter::TweetItem.new(tweet_json.merge('text' => '@joe Hey'))
40
40
  item.should_not be_publishable
41
41
  end
42
42
 
43
43
  it "should not be publishable if explicit reply" do
44
- item = Weeter::TweetItem.new(@tweet_json.merge('text' => '@joe Hey', 'in_reply_to_user_id_str' => '1'))
44
+ item = Weeter::TweetItem.new(tweet_json.merge('text' => '@joe Hey', 'in_reply_to_user_id_str' => '1'))
45
+ item.should_not be_publishable
46
+ end
47
+
48
+ it "should not be publishable if disconnect message" do
49
+ item = Weeter::TweetItem.new({"disconnect" => {"code" => 7,"stream_name" => "YappBox-statuses668638","reason" => "admin logout"}})
45
50
  item.should_not be_publishable
46
51
  end
47
52
 
48
53
  end
49
54
 
55
+ describe "limit_notice?" do
56
+ it "should be true if it's a limit notice" do
57
+ item = Weeter::TweetItem.new({ 'limit' => { 'track' => 65 }})
58
+ item.should be_limit_notice
59
+ item.missed_tweets_count.should == 65
60
+ end
61
+ it "should not be true if it's a limit notice" do
62
+ item = Weeter::TweetItem.new(tweet_json)
63
+ item.should_not be_limit_notice
64
+ lambda {
65
+ item.missed_tweets_count
66
+ }.should_not raise_error
67
+ end
68
+ end
69
+
50
70
  describe "json attributes" do
51
71
 
52
72
  it "should delegate hash calls to its json" do
@@ -63,4 +83,4 @@ describe Weeter::TweetItem do
63
83
 
64
84
 
65
85
 
66
- end
86
+ end
data/weeter.gemspec CHANGED
@@ -31,4 +31,5 @@ Gem::Specification.new do |s|
31
31
  s.add_dependency('lukemelia-twitter-stream', '~> 0.1.15')
32
32
 
33
33
  s.add_development_dependency 'rspec', '~> 2.6.0'
34
+ s.add_development_dependency 'ZenTest'
34
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: weeter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.13.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,11 +11,11 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-11-15 00:00:00.000000000Z
14
+ date: 2012-12-04 00:00:00.000000000Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: eventmachine
18
- requirement: &2154031740 !ruby/object:Gem::Requirement
18
+ requirement: &2161838440 !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
21
  - - ! '>='
@@ -23,10 +23,10 @@ dependencies:
23
23
  version: '0'
24
24
  type: :runtime
25
25
  prerelease: false
26
- version_requirements: *2154031740
26
+ version_requirements: *2161838440
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: eventmachine_httpserver
29
- requirement: &2154030760 !ruby/object:Gem::Requirement
29
+ requirement: &2161836940 !ruby/object:Gem::Requirement
30
30
  none: false
31
31
  requirements:
32
32
  - - ! '>='
@@ -34,10 +34,10 @@ dependencies:
34
34
  version: 0.2.1
35
35
  type: :runtime
36
36
  prerelease: false
37
- version_requirements: *2154030760
37
+ version_requirements: *2161836940
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: em-hiredis
40
- requirement: &2154030240 !ruby/object:Gem::Requirement
40
+ requirement: &2161835100 !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
43
  - - ! '>='
@@ -45,10 +45,10 @@ dependencies:
45
45
  version: 0.1.0
46
46
  type: :runtime
47
47
  prerelease: false
48
- version_requirements: *2154030240
48
+ version_requirements: *2161835100
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: multi_json
51
- requirement: &2154029760 !ruby/object:Gem::Requirement
51
+ requirement: &2161833640 !ruby/object:Gem::Requirement
52
52
  none: false
53
53
  requirements:
54
54
  - - ! '>='
@@ -56,10 +56,10 @@ dependencies:
56
56
  version: 1.0.2
57
57
  type: :runtime
58
58
  prerelease: false
59
- version_requirements: *2154029760
59
+ version_requirements: *2161833640
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: hashie
62
- requirement: &2154029080 !ruby/object:Gem::Requirement
62
+ requirement: &2161832500 !ruby/object:Gem::Requirement
63
63
  none: false
64
64
  requirements:
65
65
  - - ! '>='
@@ -67,10 +67,10 @@ dependencies:
67
67
  version: 1.1.0
68
68
  type: :runtime
69
69
  prerelease: false
70
- version_requirements: *2154029080
70
+ version_requirements: *2161832500
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: em-http-request
73
- requirement: &2154028360 !ruby/object:Gem::Requirement
73
+ requirement: &2161832040 !ruby/object:Gem::Requirement
74
74
  none: false
75
75
  requirements:
76
76
  - - ! '>='
@@ -78,10 +78,10 @@ dependencies:
78
78
  version: 1.0.0
79
79
  type: :runtime
80
80
  prerelease: false
81
- version_requirements: *2154028360
81
+ version_requirements: *2161832040
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: i18n
84
- requirement: &2154027700 !ruby/object:Gem::Requirement
84
+ requirement: &2161831220 !ruby/object:Gem::Requirement
85
85
  none: false
86
86
  requirements:
87
87
  - - ~>
@@ -89,10 +89,10 @@ dependencies:
89
89
  version: 0.6.0
90
90
  type: :runtime
91
91
  prerelease: false
92
- version_requirements: *2154027700
92
+ version_requirements: *2161831220
93
93
  - !ruby/object:Gem::Dependency
94
94
  name: activesupport
95
- requirement: &2154027240 !ruby/object:Gem::Requirement
95
+ requirement: &2161830060 !ruby/object:Gem::Requirement
96
96
  none: false
97
97
  requirements:
98
98
  - - ! '>='
@@ -100,10 +100,10 @@ dependencies:
100
100
  version: 3.1.1
101
101
  type: :runtime
102
102
  prerelease: false
103
- version_requirements: *2154027240
103
+ version_requirements: *2161830060
104
104
  - !ruby/object:Gem::Dependency
105
105
  name: simple_oauth
106
- requirement: &2154026740 !ruby/object:Gem::Requirement
106
+ requirement: &2161829240 !ruby/object:Gem::Requirement
107
107
  none: false
108
108
  requirements:
109
109
  - - ~>
@@ -111,10 +111,10 @@ dependencies:
111
111
  version: 0.1.5
112
112
  type: :runtime
113
113
  prerelease: false
114
- version_requirements: *2154026740
114
+ version_requirements: *2161829240
115
115
  - !ruby/object:Gem::Dependency
116
116
  name: lukemelia-twitter-stream
117
- requirement: &2154026240 !ruby/object:Gem::Requirement
117
+ requirement: &2161828460 !ruby/object:Gem::Requirement
118
118
  none: false
119
119
  requirements:
120
120
  - - ~>
@@ -122,10 +122,10 @@ dependencies:
122
122
  version: 0.1.15
123
123
  type: :runtime
124
124
  prerelease: false
125
- version_requirements: *2154026240
125
+ version_requirements: *2161828460
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: rspec
128
- requirement: &2154025660 !ruby/object:Gem::Requirement
128
+ requirement: &2161827700 !ruby/object:Gem::Requirement
129
129
  none: false
130
130
  requirements:
131
131
  - - ~>
@@ -133,7 +133,18 @@ dependencies:
133
133
  version: 2.6.0
134
134
  type: :development
135
135
  prerelease: false
136
- version_requirements: *2154025660
136
+ version_requirements: *2161827700
137
+ - !ruby/object:Gem::Dependency
138
+ name: ZenTest
139
+ requirement: &2161826960 !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ! '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: *2161826960
137
148
  description: Weeter subscribes to a set of twitter users or search terms using Twitter's
138
149
  streaming API, and notifies your app with each new tweet.
139
150
  email: