chirpstream 0.0.2 → 0.0.3

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.
data/Rakefile CHANGED
@@ -6,10 +6,10 @@ begin
6
6
  s.email = "joshbuddy@gmail.com"
7
7
  s.homepage = "http://github.com/joshbuddy/chirpstream"
8
8
  s.authors = ["Joshua Hull"]
9
- s.files = FileList["[A-Z]*", "{lib}/**/*"]
9
+ s.files = FileList["[A-Z]*", "{lib}/**/*", "{bin}/**/*"]
10
10
  s.add_dependency 'eventmachine', ">= 0.12.10"
11
11
  s.add_dependency 'em-http-request', ">= 0.2.7"
12
- s.add_dependency 'json', ">= 1.2.4"
12
+ s.add_dependency 'yajl-ruby', ">= 0.7.5"
13
13
  s.add_dependency 'load_path_find', ">= 0.0.5"
14
14
  end
15
15
  Jeweler::GemcutterTasks.new
data/Readme.rdoc CHANGED
@@ -12,8 +12,9 @@ This is an experimental EventMachine-based client for http://chirpstream.twitter
12
12
 
13
13
  chirp = Chirpstream.new('joshbuddy', 'xxxxxxxx')
14
14
  chirp.reconnect{ puts "reconnecting..." }
15
- chirp.tweet {|t| puts "#{t['text']} (from #{t['user']['name'].foreground(:red)} (#{('@' + t['user']['screen_name']).foreground(:green)}))" }
16
- chirp.follow {|t| puts "#{t.data['source'].data['screen_name'].foreground(:green)} following #{t.data['target'].data['screen_name'].foreground(:green)}" }
17
- chirp.favorite {|t| puts "#{t.data['source'].data['screen_name'].foreground(:green)} <3 -> #{t.data['target_object'].data['text']}" }
18
- chirp.retweet {|t| puts "#{t.data['source'].data['screen_name'].foreground(:green)} RT -> #{t.data['target_object'].data['text']}" }
15
+ chirp.tweet {|t| puts "#{t.text} (from #{t.user.name.foreground(:red)} (#{('@' + t.user.screen_name).foreground(:green)}))" }
16
+ chirp.follow {|t| puts "#{t.source.screen_name.foreground(:green)} following #{t.target.screen_name.foreground(:green)}" }
17
+ chirp.favorite {|t| puts "#{t.source.screen_name.foreground(:green)} <3 -> #{t.target_object.text}" }
18
+ chirp.retweet {|t| puts "#{t.source.screen_name.foreground(:green)} RT -> #{t.target_object.text}" }
19
19
  chirp.connect
20
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.2
1
+ 0.0.3
data/bin/chirp_growl ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby -rubygems
2
+
3
+ begin
4
+ require 'growl'
5
+ rescue LoadError
6
+ puts "You'll need to install growl\nsudo gem install growl"
7
+ exit(1)
8
+ end
9
+
10
+ begin
11
+ require 'json'
12
+ rescue LoadError
13
+ puts "You'll need to install json\nsudo gem install json"
14
+ exit(1)
15
+ end
16
+
17
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'chirpstream')
18
+ require 'etc'
19
+ require 'fileutils'
20
+
21
+ FileUtils.mkdir_p(File.join(Etc.getpwuid.dir, '.chirpstream'))
22
+ FileUtils.mkdir_p(File.join(Etc.getpwuid.dir, '.chirpstream', 'cache'))
23
+
24
+ json_file = File.join(Etc.getpwuid.dir, '.chirpstream', 'user.json')
25
+ cache_dir = File.join(Etc.getpwuid.dir, '.chirpstream', 'cache')
26
+
27
+ if File.exist?(json_file)
28
+ data = JSON.parse(File.read(json_file))
29
+ @username = data['username']
30
+ @password = data['password']
31
+ else
32
+ puts "Enter your username:"
33
+ @username = gets.strip
34
+ puts "Enter your password:"
35
+ @password = gets.strip
36
+ File.open(json_file, 'w') {|f| f << {:username => @username, :password => @password}.to_json}
37
+ end
38
+
39
+ chirp = Chirpstream.new(@username, @password)
40
+
41
+ Growl.notify {
42
+ self.message = 'Starting chirp_growl...'
43
+ self.title = 'ChirpStreeeeem!'
44
+ self.image = File.join(File.dirname(__FILE__), '..', 'image', 'bird.png')
45
+ }
46
+
47
+ chirp.tweet { |t|
48
+ t.user.with_profile_image(cache_dir) do |image_path|
49
+ Growl.notify {
50
+ self.message = t.text
51
+ self.title = "@#{t.user.screen_name}"
52
+ self.image = image_path if image_path
53
+ }
54
+ end
55
+ }
56
+
57
+ chirp.connect
58
+
59
+
@@ -1,4 +1,7 @@
1
1
  class Chirpstream
2
2
  class Delete < Event
3
+ ATTRS = [:id, :user_id]
4
+ attr_accessor *ATTRS
5
+ user_writer :user_id
3
6
  end
4
7
  end
@@ -1,4 +1,8 @@
1
1
  class Chirpstream
2
2
  class Favorite < Event
3
+ ATTRS = [ :target, :source, :target_object ]
4
+ attr_accessor *ATTRS
5
+ user_writer :target, :source
6
+ tweet_writer :target_object
3
7
  end
4
8
  end
@@ -1,4 +1,7 @@
1
1
  class Chirpstream
2
2
  class Follow < Event
3
+ ATTRS = [ :target, :source ]
4
+ attr_accessor *ATTRS
5
+ user_writer :target, :source
3
6
  end
4
7
  end
@@ -1,4 +1,8 @@
1
1
  class Chirpstream
2
2
  class Retweet < Event
3
+ ATTRS = [ :target, :source, :target_object ]
4
+ attr_accessor *ATTRS
5
+ user_writer :target, :source
6
+ tweet_writer :target_object
3
7
  end
4
8
  end
@@ -1,19 +1,5 @@
1
1
  class Chirpstream
2
2
 
3
- class Event
4
-
5
- attr_reader :data
6
-
7
- def initialize(data)
8
- @data = data
9
- end
10
-
11
- def load_all(&block)
12
- if unloaded_data = @data.values.find{|d| d.respond_to?(:loaded?) && !d.loaded? }
13
- unloaded_data.load { load_all(&block) }
14
- else
15
- block.call(self)
16
- end
17
- end
3
+ class Event < TwitterObject
18
4
  end
19
5
  end
@@ -1,29 +1,17 @@
1
1
  class Chirpstream
2
- class Tweet
2
+ class Tweet < TwitterObject
3
3
 
4
- URL = "http://api.twitter.com/1/statuses/show/%s.json"
5
-
6
- attr_reader :id, :data
7
-
8
- def initialize(base, id)
9
- @base = base
10
- @id = id
11
- end
12
-
13
- def load
14
- unless loaded?
15
- http = EM::HttpRequest.new(URL % id).get(:head => {'authorization' => [@base.username, @base.password]})
16
- http.callback {
17
- if http.response_header.status == 200
18
- @data = JSON.parse(http.response)
19
- yield self
20
- end
21
- }
22
- end
4
+ ATTRS = [:coordinates, :favorited, :created_at, :truncated, :contributors, :text, :id, :geo, :in_reply_to_user_id, :place, :source, :in_reply_to_screen_name, :in_reply_to_status_id, :user]
5
+
6
+ attr_accessor *ATTRS
7
+ user_writer :user
8
+
9
+ def tweet_loadable_id
10
+ id
23
11
  end
24
12
 
25
13
  def loaded?
26
- @data
14
+ !text.nil?
27
15
  end
28
16
  end
29
17
  end
@@ -0,0 +1,135 @@
1
+ class TwitterObject
2
+
3
+ SINGLE_USER_URL = "http://api.twitter.com/1/users/show/%s.json"
4
+ SINGLE_TWEET_URL = "http://api.twitter.com/1/statuses/show/%s.json"
5
+
6
+ attr_reader :base
7
+
8
+ def initialize(base, data = nil)
9
+ @base = base
10
+ from_json(data) if data
11
+ yield self if block_given?
12
+ end
13
+
14
+ def self.user_writer(*attrs)
15
+ attrs.each do |attr|
16
+ module_eval "
17
+ def #{attr}=(#{attr})
18
+ @#{attr} = if #{attr}.is_a?(Hash)
19
+ Chirpstream::User.new(base, #{attr})
20
+ else
21
+ Chirpstream::User.new(base) {|u| u.id = #{attr}}
22
+ end
23
+ end
24
+ "
25
+ end
26
+ end
27
+
28
+ def self.tweet_writer(*attrs)
29
+ attrs.each do |attr|
30
+ module_eval "
31
+ def #{attr}=(#{attr})
32
+ @#{attr} = if #{attr}.is_a?(Hash)
33
+ Chirpstream::Tweet.new(base, #{attr})
34
+ else
35
+ Chirpstream::Tweet.new(base) {|t| t.id = #{attr}}
36
+ end
37
+ end
38
+ "
39
+ end
40
+ end
41
+
42
+ def from_json(data)
43
+ self.class.attrs.each { |a| self.send(:"#{a}=", data[a.to_s]) }
44
+ end
45
+
46
+ def self.attrs
47
+ const_get(:ATTRS)
48
+ end
49
+
50
+ def load_all(&block)
51
+ attrs = self.class.attrs
52
+ if respond_to?(:loaded?) && !loaded?
53
+ if respond_to?(:user_loadable_id)
54
+ from_json(get_user_data(user_loadable_id)[user_loadable_id])
55
+ load_all(&block)
56
+ elsif respond_to?(:tweet_loadable_id)
57
+ from_json(get_tweet_data(tweet_loadable_id)[tweet_loadable_id])
58
+ load_all(&block)
59
+ end
60
+ else
61
+ tweet_ids = {}
62
+ user_ids = {}
63
+ attrs.each do |a|
64
+ obj = send(a)
65
+ if obj.respond_to?(:loaded?) && !obj.loaded?
66
+ if obj.respond_to?(:user_loadable_id)
67
+ user_ids[a] = obj.user_loadable_id
68
+ elsif obj.respond_to?(:tweet_loadable_id)
69
+ tweet_ids[a] = obj.tweet_loadable_id
70
+ end
71
+ end
72
+ end
73
+ get_tweet_data(tweet_ids.values) { |tweet_data|
74
+ tweet_ids.each{|k,v| self.send(:"#{k}=", tweet_data[v])}
75
+ user_data = get_user_data(user_ids.values) { |user_data|
76
+ user_ids.each{|k,v| self.send(:"#{k}=", user_data[v])}
77
+ yield self
78
+ }
79
+ }
80
+ end
81
+
82
+ end
83
+
84
+ def get_tweet_data(ids)
85
+ ids = Array(ids).uniq
86
+ data = {}
87
+ if ids.empty?
88
+ yield data
89
+ else
90
+ load_tweet_data(ids, data) { yield data }
91
+ end
92
+ end
93
+
94
+ def load_tweet_data(ids, data, &block)
95
+ id = ids.shift
96
+ if (id)
97
+ parser = Yajl::Parser.new
98
+ parser.on_parse_complete = proc { |parsed|
99
+ data[id] = parsed
100
+ load_tweet_data(ids, data, &block)
101
+ }
102
+ http = EM::HttpRequest.new("http://api.twitter.com/1/statuses/show/%s.json" % id).get :head => {'authorization' => [base.username, base.password]}
103
+ http.stream { |chunk|
104
+ parser << chunk
105
+ }
106
+ else
107
+ yield
108
+ end
109
+ end
110
+
111
+ def get_user_data(ids)
112
+ ids = Array(ids).uniq
113
+ data = {}
114
+ if ids.empty?
115
+ yield data
116
+ else
117
+ load_user_data(ids, data) { yield data }
118
+ end
119
+ end
120
+
121
+ def load_user_data(ids, data)
122
+ parser = Yajl::Parser.new
123
+ parser.on_parse_complete = proc { |parsed|
124
+ parsed.each do |user|
125
+ data[user["id"]] = user
126
+ end
127
+ yield
128
+ }
129
+ http = EM::HttpRequest.new("http://api.twitter.com/1/users/lookup.json").post :head => {'authorization' => [base.username, base.password]}, :body => {'user_id' => ids.join(',')}
130
+ http.stream { |chunk|
131
+ parser << chunk
132
+ }
133
+ end
134
+
135
+ end
@@ -1,29 +1,46 @@
1
+ require 'em-http'
2
+ require 'digest/md5'
3
+
1
4
  class Chirpstream
2
- class User
3
-
4
- URL = "http://api.twitter.com/1/users/show/%s.json"
5
-
6
- attr_reader :id, :data
5
+ class User < TwitterObject
7
6
 
8
- def initialize(base, id)
9
- @base = base
10
- @id = id
7
+ ATTRS = [
8
+ :profile_background_tile, :name, :profile_sidebar_border_color, :profile_sidebar_fill_color, :profile_image_url, :location, :created_at,
9
+ :profile_link_color, :url, :contributors_enabled, :favourites_count, :utc_offset, :id, :followers_count, :protected, :lang,
10
+ :profile_text_color, :geo_enabled, :profile_background_color, :time_zone, :notifications, :description, :verified, :profile_background_image_url,
11
+ :statuses_count, :friends_count, :screen_name, :following
12
+ ]
13
+
14
+ attr_accessor *ATTRS
15
+
16
+ def loaded?
17
+ name
18
+ end
19
+
20
+ def user_loadable_id
21
+ id
11
22
  end
12
-
13
- def load
14
- unless loaded?
15
- http = EM::HttpRequest.new(URL % id).get(:head => {'authorization' => [@base.username, @base.password]})
16
- http.callback {
23
+
24
+ def with_profile_image(cache_dir)
25
+ raise unless loaded?
26
+
27
+ cached_file = File.join(cache_dir, "#{Digest::MD5.hexdigest(profile_image_url)}#{File.extname(profile_image_url)}")
28
+
29
+ if File.exist?(cached_file)
30
+ yield cached_file
31
+ else
32
+ http = EM::HttpRequest.new(profile_image_url).get :head => {'authorization' => [base.username, base.password]}
33
+ http.callback do
17
34
  if http.response_header.status == 200
18
- @data = JSON.parse(http.response)
19
- yield self
35
+ File.open(cached_file, 'w') {|f| f << http.response}
36
+ yield cached_file
37
+ else
38
+ yield nil
20
39
  end
21
- }
40
+ end
22
41
  end
42
+
23
43
  end
24
-
25
- def loaded?
26
- @data
27
- end
44
+
28
45
  end
29
46
  end
data/lib/chirpstream.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  require 'eventmachine'
2
2
  require 'em-http'
3
- require 'json'
3
+ require 'yajl'
4
4
  require 'pp'
5
5
  require 'load_path_find'
6
6
 
7
7
  $LOAD_PATH.add_current
8
8
 
9
+ require 'chirpstream/twitter_object'
9
10
  require 'chirpstream/event'
10
11
  require 'chirpstream/event/follow'
11
12
  require 'chirpstream/event/retweet'
@@ -61,63 +62,63 @@ class Chirpstream
61
62
  def dispatch_friend(data)
62
63
  unless @handlers.friend.empty?
63
64
  data['friends'].each_slice(100) do |friend_ids|
64
- friend_http = EM::HttpRequest.new("http://api.twitter.com/1/users/lookup.json").post(:body => {'user_id' => friend_ids.join(',')}, :head => {'authorization' => [@username, @password]})
65
- friend_http.callback do
66
- friends = JSON.parse(friend_http.response)
65
+ parser = Yajl::Parser.new
66
+ parser.on_parse_complete = proc { |friends|
67
67
  friends.each do |friend|
68
68
  @handlers.friend.each{|h| h.call(friend)}
69
69
  end
70
- end
70
+ }
71
+ friend_http = EM::HttpRequest.new("http://api.twitter.com/1/users/lookup.json").post(:body => {'user_id' => friend_ids.join(',')}, :head => {'authorization' => [@username, @password]})
72
+ http.stream { |chunk|
73
+ parser << chunk
74
+ }
71
75
  end
72
76
  end
73
77
  end
74
78
 
75
79
  def dispatch_tweet(data)
76
80
  unless @handlers.tweet.empty?
77
- @handlers.tweet.each{|h| h.call(data)}
81
+ tweet = Tweet.new(self, data)
82
+ tweet.load_all { |t|
83
+ @handlers.tweet.each{|h| h.call(tweet)}
84
+ }
78
85
  end
79
86
  end
80
87
 
81
88
  def dispatch_follow(data)
82
89
  unless @handlers.follow.empty?
83
- data['target'] = User.new(self, data['target']['id'])
84
- data['source'] = User.new(self, data['source']['id'])
85
- e = Follow.new(data)
86
- e.load_all {
87
- @handlers.follow.each{|h| h.call(e)}
90
+ follow = Follow.new(self, data)
91
+ follow.load_all { |f|
92
+ @handlers.follow.each{|h| h.call(f)}
88
93
  }
89
94
  end
90
95
  end
91
96
 
92
97
  def dispatch_favorite(data)
93
- return if @handlers.follow.empty?
94
- data['target'] = User.new(self, data['target']['id'])
95
- data['source'] = User.new(self, data['source']['id'])
96
- data['target_object'] = Tweet.new(self, data['target_object']['id'])
97
- e = Favorite.new(data)
98
- e.load_all {
99
- @handlers.favorite.each{|h| h.call(e)}
100
- }
98
+ unless @handlers.favorite.empty?
99
+ favorite = Favorite.new(self, data)
100
+ favorite.load_all { |f|
101
+ @handlers.favorite.each{|h| h.call(f)}
102
+ }
103
+ end
101
104
  end
102
105
 
103
106
  def dispatch_retweet(data)
104
- return if @handlers.retweet.empty?
105
- data['target'] = User.new(self, data['target']['id'])
106
- data['source'] = User.new(self, data['source']['id'])
107
- data['target_object'] = Tweet.new(self, data['target_object']['id'])
108
- e = Retweet.new(data)
109
- e.load_all {
110
- @handlers.retweet.each{|h| h.call(e)}
111
- }
107
+ unless @handlers.retweet.empty?
108
+ retweet = Retweet.new(self, data)
109
+ retweet.load_all { |f|
110
+ @handlers.retweet.each{|h| h.call(f)}
111
+ }
112
+ end
112
113
  end
113
114
 
114
115
  def dispatch_delete(data)
115
- return if @handlers.delete.empty?
116
- data['delete']['user_id'] = User.new(self, data['delete']['user_id'])
117
- e = Delete.new(data)
118
- e.load_all {
119
- @handlers.delete.each{|h| h.call(e)}
120
- }
116
+ unless @handlers.delete.empty?
117
+ delete = Delete.new(self, data)
118
+ delete.load_all { |f|
119
+ @handlers.delete.each{|h| h.call(f)}
120
+ }
121
+ end
121
122
  end
122
123
 
123
124
  def dispatch_reconnect
@@ -125,45 +126,44 @@ class Chirpstream
125
126
  @handlers.reconnect.each{|h| h.call}
126
127
  end
127
128
 
129
+ def handle_tweet(parsed_data)
130
+ if parsed_data['friends']
131
+ dispatch_friend(parsed_data)
132
+ elsif parsed_data['text']
133
+ dispatch_tweet(parsed_data)
134
+ elsif parsed_data['event']
135
+ case parsed_data['event']
136
+ when 'follow'
137
+ dispatch_follow(parsed_data)
138
+ when 'favorite'
139
+ dispatch_favorite(parsed_data)
140
+ when 'retweet'
141
+ dispatch_retweet(parsed_data)
142
+ else
143
+ puts "weird event"
144
+ pp parsed_data
145
+ end
146
+ elsif parsed_data['delete']
147
+ dispatch_delete(parsed_data)
148
+ else
149
+ puts "i didn't know what to do with this!"
150
+ pp parsed_data
151
+ end
152
+ end
153
+
128
154
  def connect
129
155
  unless EM.reactor_running?
130
156
  EM.run { connect }
131
157
  else
158
+ parser = Yajl::Parser.new
159
+ parser.on_parse_complete = method(:handle_tweet)
132
160
  http = EM::HttpRequest.new(@connect_url).get :head => {'authorization' => [@username, @password]}
133
161
  http.errback { |e, err|
134
162
  dispatch_reconnect
135
163
  connect
136
164
  }
137
- http.stream { |chunk|
138
- @data << chunk
139
- begin
140
- parsed_data = JSON.parse(@data)
141
- @data = ''
142
- if parsed_data['friends']
143
- dispatch_friend(parsed_data)
144
- elsif parsed_data['text']
145
- dispatch_tweet(parsed_data)
146
- elsif parsed_data['event']
147
- case parsed_data['event']
148
- when 'follow'
149
- dispatch_follow(parsed_data)
150
- when 'favorite'
151
- dispatch_favorite(parsed_data)
152
- when 'retweet'
153
- dispatch_retweet(parsed_data)
154
- else
155
- puts "weird event"
156
- pp parsed_data
157
- end
158
- elsif parsed_data['delete']
159
- dispatch_delete(parsed_data)
160
- else
161
- puts "i didn't know what to do with this!"
162
- pp parsed_data
163
- end
164
- rescue JSON::ParserError
165
- #puts "need more"
166
- end
165
+ http.stream { |chunk|
166
+ parser << chunk
167
167
  }
168
168
  end
169
169
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 2
9
- version: 0.0.2
8
+ - 3
9
+ version: 0.0.3
10
10
  platform: ruby
11
11
  authors:
12
12
  - Joshua Hull
@@ -14,8 +14,8 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-04-22 00:00:00 -04:00
18
- default_executable:
17
+ date: 2010-05-01 00:00:00 -04:00
18
+ default_executable: chirp_growl
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: eventmachine
@@ -46,17 +46,17 @@ dependencies:
46
46
  type: :runtime
47
47
  version_requirements: *id002
48
48
  - !ruby/object:Gem::Dependency
49
- name: json
49
+ name: yajl-ruby
50
50
  prerelease: false
51
51
  requirement: &id003 !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - ">="
54
54
  - !ruby/object:Gem::Version
55
55
  segments:
56
- - 1
57
- - 2
58
- - 4
59
- version: 1.2.4
56
+ - 0
57
+ - 7
58
+ - 5
59
+ version: 0.7.5
60
60
  type: :runtime
61
61
  version_requirements: *id003
62
62
  - !ruby/object:Gem::Dependency
@@ -75,8 +75,8 @@ dependencies:
75
75
  version_requirements: *id004
76
76
  description: Eventmachine-based Chirpstream client
77
77
  email: joshbuddy@gmail.com
78
- executables: []
79
-
78
+ executables:
79
+ - chirp_growl
80
80
  extensions: []
81
81
 
82
82
  extra_rdoc_files: []
@@ -86,14 +86,15 @@ files:
86
86
  - Rakefile
87
87
  - Readme.rdoc
88
88
  - VERSION
89
+ - bin/chirp_growl
89
90
  - lib/chirpstream.rb
90
91
  - lib/chirpstream/event.rb
91
92
  - lib/chirpstream/event/delete.rb
92
93
  - lib/chirpstream/event/favorite.rb
93
94
  - lib/chirpstream/event/follow.rb
94
95
  - lib/chirpstream/event/retweet.rb
95
- - lib/chirpstream/loadable.rb
96
96
  - lib/chirpstream/tweet.rb
97
+ - lib/chirpstream/twitter_object.rb
97
98
  - lib/chirpstream/user.rb
98
99
  has_rdoc: true
99
100
  homepage: http://github.com/joshbuddy/chirpstream
File without changes