tweetable 0.1.10
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/Gemfile +8 -0
- data/History.txt +4 -0
- data/Manifest.txt +33 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +37 -0
- data/Rakefile +19 -0
- data/VERSION +1 -0
- data/lib/tweetable/authorization.rb +21 -0
- data/lib/tweetable/collection.rb +54 -0
- data/lib/tweetable/link.rb +30 -0
- data/lib/tweetable/message.rb +119 -0
- data/lib/tweetable/persistable.rb +32 -0
- data/lib/tweetable/photo.rb +6 -0
- data/lib/tweetable/queue.rb +121 -0
- data/lib/tweetable/search.rb +77 -0
- data/lib/tweetable/twitter_client.rb +36 -0
- data/lib/tweetable/twitter_streaming_client.rb +67 -0
- data/lib/tweetable/url.rb +39 -0
- data/lib/tweetable/user.rb +104 -0
- data/lib/tweetable.rb +82 -0
- data/pkg/tweetable-0.1.7.gem +0 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/collection_spec.rb +55 -0
- data/spec/fixtures/blank.json +1 -0
- data/spec/fixtures/flippyhead.json +1 -0
- data/spec/fixtures/follower_ids.json +1 -0
- data/spec/fixtures/friend_ids.json +1 -0
- data/spec/fixtures/friends_timeline.json +1 -0
- data/spec/fixtures/link_blank.json +1 -0
- data/spec/fixtures/link_exists.json +1 -0
- data/spec/fixtures/rate_limit_status.json +1 -0
- data/spec/fixtures/search.json +1 -0
- data/spec/fixtures/user_timeline.json +1 -0
- data/spec/fixtures/verify_credentials.json +1 -0
- data/spec/link_spec.rb +35 -0
- data/spec/message_spec.rb +148 -0
- data/spec/persistable_spec.rb +53 -0
- data/spec/queue_spec.rb +29 -0
- data/spec/search_spec.rb +60 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/tweetable_spec.rb +19 -0
- data/spec/twitter_client_spec.rb +41 -0
- data/spec/twitter_streaming_client_spec.rb +18 -0
- data/spec/user_spec.rb +143 -0
- data/tweetable.gemspec +106 -0
- metadata +165 -0
data/Gemfile
ADDED
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
PostInstall.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
6
|
+
lib/tweetable.rb
|
7
|
+
lib/tweetable/authorization.rb
|
8
|
+
lib/tweetable/collection.rb
|
9
|
+
lib/tweetable/link.rb
|
10
|
+
lib/tweetable/message.rb
|
11
|
+
lib/tweetable/persistable.rb
|
12
|
+
lib/tweetable/photo.rb
|
13
|
+
lib/tweetable/queue.rb
|
14
|
+
lib/tweetable/search.rb
|
15
|
+
lib/tweetable/twitter_client.rb
|
16
|
+
lib/tweetable/twitter_streaming_client.rb
|
17
|
+
lib/tweetable/url.rb
|
18
|
+
lib/tweetable/user.rb
|
19
|
+
script/console
|
20
|
+
script/destroy
|
21
|
+
script/generate
|
22
|
+
spec/collection_spec.rb
|
23
|
+
spec/link_spec.rb
|
24
|
+
spec/message_spec.rb
|
25
|
+
spec/persistable_spec.rb
|
26
|
+
spec/queue_spec.rb
|
27
|
+
spec/search_spec.rb
|
28
|
+
spec/spec.opts
|
29
|
+
spec/spec_helper.rb
|
30
|
+
spec/twitter_client_spec.rb
|
31
|
+
spec/twitter_streaming_client_spec.rb
|
32
|
+
spec/user_spec.rb
|
33
|
+
tweetable.gemspec
|
data/PostInstall.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
== DESCRIPTION:
|
2
|
+
|
3
|
+
== EXAMPLES:
|
4
|
+
|
5
|
+
To create a new Tweetable user:
|
6
|
+
|
7
|
+
@user = Tweetable::User.create(:screen_name => 'flippyhead')
|
8
|
+
|
9
|
+
To then grab recent messages, friend counts, and other profile data:
|
10
|
+
|
11
|
+
@user.update_all # will only grab messages since the last known message
|
12
|
+
|
13
|
+
Now you have access to stuff like:
|
14
|
+
|
15
|
+
@user.friend_ids # [34102, 23423, 67567, etc...]
|
16
|
+
@user.friend_ids.size # 102
|
17
|
+
@user.profile_image_url # http://twitter.com/...
|
18
|
+
@user.messages.size # 202
|
19
|
+
|
20
|
+
Links in messages can be extracted and expanded:
|
21
|
+
|
22
|
+
@message = @user.messages.first
|
23
|
+
@message.parse_links
|
24
|
+
|
25
|
+
@link = @message.links.first # Tweetable::Link
|
26
|
+
@link.url # http://tinyurl.com/yfuhltt
|
27
|
+
@link.long_url # http://pathable.com
|
28
|
+
|
29
|
+
And are connected to other users who mention them:
|
30
|
+
|
31
|
+
@message.links.size # 2
|
32
|
+
@link.count # 8 (uses discovered so far)
|
33
|
+
@link.users # [<Tweetable::User:0x1 @_attributes={}>, ...]
|
34
|
+
|
35
|
+
Performing a keyword search is just as easy:
|
36
|
+
|
37
|
+
# @search =
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gemspec|
|
8
|
+
gemspec.name = "tweetable"
|
9
|
+
gemspec.summary = "Track twitter messages and users in memory using Redis"
|
10
|
+
gemspec.description = "Track twitter messages and users in memory using Redis"
|
11
|
+
gemspec.email = "peter@flippyhead.com"
|
12
|
+
gemspec.homepage = "http://github.com/flippyhead/tweetable"
|
13
|
+
gemspec.authors = ["Peter T. Brown"]
|
14
|
+
gemspec.add_bundler_dependencies
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.10
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Tweetable
|
2
|
+
class Authorization < Persistable
|
3
|
+
attribute :oauth_access_token
|
4
|
+
attribute :oauth_access_secret
|
5
|
+
|
6
|
+
index :oauth_access_token
|
7
|
+
|
8
|
+
def user_id
|
9
|
+
return if self.oauth_access_token.nil?
|
10
|
+
self.oauth_access_token.split('-')[0] # tokens start with ID as in: 13705052-bz9IrOTwWbLgWHQDKkGnVd815ybTujc0QeXMlh7ZJ
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def validate
|
16
|
+
assert_present :oauth_access_token
|
17
|
+
assert_present :oauth_access_secret
|
18
|
+
assert_unique :oauth_access_token
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Tweetable
|
2
|
+
class Collection < Persistable
|
3
|
+
attribute :created_at
|
4
|
+
attribute :updated_at
|
5
|
+
attribute :name # super class attributes don't get picked up in subclasses for some reason
|
6
|
+
index :name
|
7
|
+
|
8
|
+
def validate
|
9
|
+
assert_present :name
|
10
|
+
assert_unique :name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class UserCollection < Collection
|
15
|
+
attribute :created_at
|
16
|
+
attribute :updated_at
|
17
|
+
attribute :name
|
18
|
+
index :name
|
19
|
+
|
20
|
+
set :user_set, User
|
21
|
+
list :users, User
|
22
|
+
end
|
23
|
+
|
24
|
+
class MessageCollection < Collection
|
25
|
+
attribute :created_at
|
26
|
+
attribute :updated_at
|
27
|
+
attribute :name
|
28
|
+
index :name
|
29
|
+
|
30
|
+
set :message_set, Message
|
31
|
+
list :messages, Message
|
32
|
+
end
|
33
|
+
|
34
|
+
class SearchCollection < Collection
|
35
|
+
attribute :created_at
|
36
|
+
attribute :updated_at
|
37
|
+
attribute :name
|
38
|
+
index :name
|
39
|
+
|
40
|
+
set :search_set, Search
|
41
|
+
list :searches, Search
|
42
|
+
end
|
43
|
+
|
44
|
+
class LinkCollection < Collection
|
45
|
+
attribute :created_at
|
46
|
+
attribute :updated_at
|
47
|
+
attribute :name
|
48
|
+
index :name
|
49
|
+
|
50
|
+
set :link_set, Link
|
51
|
+
list :links, Link
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Tweetable
|
2
|
+
class Link < Persistable
|
3
|
+
URL_PATTERN = /(http:\S+)/ix
|
4
|
+
|
5
|
+
attribute :created_at
|
6
|
+
attribute :updated_at
|
7
|
+
attribute :url
|
8
|
+
attribute :long_url
|
9
|
+
|
10
|
+
index :url
|
11
|
+
index :long_url
|
12
|
+
|
13
|
+
# set :messages, Tweetable::Message
|
14
|
+
set :users, Tweetable::User
|
15
|
+
counter :count
|
16
|
+
|
17
|
+
def increment_usage_count(user)
|
18
|
+
return false if (user.nil? || self.users.include?(user))
|
19
|
+
users.add(user)
|
20
|
+
self.incr(:count)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def validate
|
26
|
+
assert_present :url
|
27
|
+
assert_unique :url
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Tweetable
|
2
|
+
class Message < Persistable
|
3
|
+
attr_accessor :owner
|
4
|
+
|
5
|
+
attribute :created_at
|
6
|
+
attribute :updated_at
|
7
|
+
attribute :message_id
|
8
|
+
attribute :text
|
9
|
+
attribute :favorited
|
10
|
+
attribute :from_user_id
|
11
|
+
attribute :from_screen_name
|
12
|
+
attribute :links_parsed
|
13
|
+
attribute :photos_parsed
|
14
|
+
attribute :sent_at
|
15
|
+
attribute :created_at
|
16
|
+
|
17
|
+
set :links, Link
|
18
|
+
set :photos, Photo
|
19
|
+
set :tags
|
20
|
+
|
21
|
+
index :message_id
|
22
|
+
index :links_parsed
|
23
|
+
index :photos_parsed
|
24
|
+
|
25
|
+
def self.create_from_timeline(message, create_user = false)
|
26
|
+
m = Message.find_or_create(:message_id, message[:id])
|
27
|
+
|
28
|
+
m.update(
|
29
|
+
:favorited => message.favorited,
|
30
|
+
:photos_parsed => '0',
|
31
|
+
:links_parsed => '0',
|
32
|
+
:created_at => Time.now.utc.to_s,
|
33
|
+
:sent_at => message.created_at,
|
34
|
+
:text => message.text,
|
35
|
+
:from_screen_name => message.user.screen_name.downcase,
|
36
|
+
:from_user_id => message.user[:id])
|
37
|
+
|
38
|
+
if create_user and m.valid?
|
39
|
+
u = User.create_from_timeline(message.user)
|
40
|
+
u.messages << m if u.valid?
|
41
|
+
end
|
42
|
+
m
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.create_from_status(text, client)
|
46
|
+
self.create_from_timeline(client.update(text), true)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.purge(&block)
|
50
|
+
all.sort.each do |message|
|
51
|
+
message.delete if yield(message)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def from_user
|
56
|
+
return nil if self.from_screen_name.nil?
|
57
|
+
User.find(:screen_name => self.from_screen_name.downcase).first
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_links(force = false, longify = true)
|
61
|
+
return unless (force or self.links_parsed != '1')
|
62
|
+
|
63
|
+
urls = self.text.scan(Link::URL_PATTERN).flatten
|
64
|
+
urls.each do |url|
|
65
|
+
link = Link.find(:url => url).first
|
66
|
+
|
67
|
+
if !link
|
68
|
+
link = Link.create(:url => url, :created_at => Time.new.to_s)
|
69
|
+
next if !link.valid?
|
70
|
+
|
71
|
+
# link.messages.add(self)
|
72
|
+
long_url = URL.lookup_long_url(url)
|
73
|
+
link.update(:long_url => long_url) unless (long_url.nil? or long_url == url)
|
74
|
+
end
|
75
|
+
|
76
|
+
link.increment_usage_count(from_user)
|
77
|
+
|
78
|
+
update(:links_parsed => '1')
|
79
|
+
links.add(link)
|
80
|
+
end
|
81
|
+
|
82
|
+
links
|
83
|
+
end
|
84
|
+
|
85
|
+
def twitter_link
|
86
|
+
"http://twitter.com/#{from_screen_name}/status/#{message_id}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def validate
|
90
|
+
super
|
91
|
+
assert_unique :message_id
|
92
|
+
assert_present :text
|
93
|
+
assert_format :links_parsed, /^[0,1]$/
|
94
|
+
assert_format :photos_parsed, /^[0,1]$/
|
95
|
+
end
|
96
|
+
|
97
|
+
def hash
|
98
|
+
self.id.hash
|
99
|
+
end
|
100
|
+
|
101
|
+
# Simply delegate to == in this example.
|
102
|
+
def eql?(comparee)
|
103
|
+
self == comparee
|
104
|
+
end
|
105
|
+
|
106
|
+
# Objects are equal if they have the same
|
107
|
+
# unique custom identifier.
|
108
|
+
def ==(comparee)
|
109
|
+
self.id == comparee.id
|
110
|
+
end
|
111
|
+
|
112
|
+
# It seems that, at least using streaming, message id's are not sequential anymore
|
113
|
+
# So comparisons are done on the official sent_at date/time
|
114
|
+
def <=>(o)
|
115
|
+
return 1 if o.nil?
|
116
|
+
Time.parse(self.sent_at) <=> Time.parse(o.sent_at)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'ohm'
|
2
|
+
|
3
|
+
module Tweetable
|
4
|
+
class Persistable < Ohm::Model
|
5
|
+
attribute :created_at
|
6
|
+
attribute :updated_at
|
7
|
+
|
8
|
+
def self.find_or_create(key, value)
|
9
|
+
attributes = {key => value} # this persistable uses an old interface
|
10
|
+
models = self.find(attributes)
|
11
|
+
models.empty? ? self.create(attributes.merge(:created_at => Time.now.utc.to_s)) : models.first
|
12
|
+
end
|
13
|
+
|
14
|
+
def needs_update?(force = false)
|
15
|
+
force or self.updated_at.blank? or (Time.parse(self.updated_at) + self.config[:update_delay]) < Time.now.utc
|
16
|
+
end
|
17
|
+
|
18
|
+
def client
|
19
|
+
Tweetable.client
|
20
|
+
end
|
21
|
+
|
22
|
+
def config
|
23
|
+
Tweetable.config
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def validate
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Tweetable
|
2
|
+
class Queue
|
3
|
+
def self.clear_search_queue(queue_name)
|
4
|
+
collection = Tweetable::SearchCollection.find(:name, queue_name).first
|
5
|
+
return true if collection.nil?
|
6
|
+
collection.searches.size.times{|i| collection.searches.pop}
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.clear_user_queue(queue_name)
|
11
|
+
collection = Tweetable::UserCollection.find(:name, queue_name).first
|
12
|
+
return true if collection.nil?
|
13
|
+
collection.users.size.times{|i| collection.users.pop}
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.add_to_search_queue(queue_name, queries, &block)
|
18
|
+
queue = Tweetable::SearchCollection.find_or_create(:name, queue_name)
|
19
|
+
return unless queue.searches.empty?
|
20
|
+
|
21
|
+
queries.each do |query|
|
22
|
+
search = Tweetable::Search.find_or_create(:query, query)
|
23
|
+
|
24
|
+
yield search if block_given?
|
25
|
+
|
26
|
+
queue.searches << search.id
|
27
|
+
end
|
28
|
+
|
29
|
+
queue
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.add_to_user_queue(queue_name, screen_names, &block)
|
33
|
+
queue = Tweetable::UserCollection.find_or_create(:name, queue_name)
|
34
|
+
return unless queue.users.empty?
|
35
|
+
|
36
|
+
screen_names.each do |screen_name|
|
37
|
+
user = Tweetable::User.find_or_create(:screen_name, screen_name)
|
38
|
+
|
39
|
+
yield user if block_given?
|
40
|
+
|
41
|
+
queue.users << user.id
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.pull_from_search_queue(queue_name)
|
46
|
+
queue = Tweetable::SearchCollection.find(:name, queue_name).first
|
47
|
+
return 0 if (queue.nil? or queue.searches.empty?)
|
48
|
+
|
49
|
+
count = 0
|
50
|
+
while !queue.searches.empty?
|
51
|
+
search = Tweetable::Search[queue.searches.pop] # have to find object by id in List
|
52
|
+
process_search(search)
|
53
|
+
count += 1
|
54
|
+
end
|
55
|
+
|
56
|
+
return count
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.pull_from_user_queue(queue_name)
|
60
|
+
queue = Tweetable::UserCollection.find(:name, queue_name).first
|
61
|
+
return if (queue.nil? or queue.users.empty?)
|
62
|
+
|
63
|
+
count = 0
|
64
|
+
while !queue.users.empty?
|
65
|
+
user = Tweetable::User[queue.users.pop] # have to find object by id in List
|
66
|
+
process_user(user)
|
67
|
+
count += 1
|
68
|
+
end
|
69
|
+
|
70
|
+
return count
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.process_search(search)
|
74
|
+
pull_from_queue_safely do
|
75
|
+
return search.update_all(true)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.process_user(user)
|
80
|
+
pull_from_queue_safely do
|
81
|
+
messages = user.update_all(true)
|
82
|
+
user.tags.each do |tag|
|
83
|
+
message_collection = Tweetable::MessageCollection.find_or_create(:name, tag)
|
84
|
+
user_collection = Tweetable::UserCollection.find_or_create(:name, tag)
|
85
|
+
|
86
|
+
message_collection.update(:updated_at => Time.now.utc.to_s)
|
87
|
+
user_collection.update(:updated_at => Time.now.utc.to_s)
|
88
|
+
|
89
|
+
user_collection.user_set.add(user)
|
90
|
+
messages.each{|m| message_collection.messages << m.id}
|
91
|
+
end
|
92
|
+
return messages
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.pull_from_queue_safely
|
97
|
+
begin
|
98
|
+
yield
|
99
|
+
rescue Tweetable::TweetableError => e
|
100
|
+
raise TemporaryPullFromQueueError.new("Twitter unavailable error: #{e}")
|
101
|
+
rescue Twitter::Unavailable => e
|
102
|
+
raise TemporaryPullFromQueueError.new("Twitter unavailable error: #{e}")
|
103
|
+
rescue URI::InvalidURIError => e
|
104
|
+
raise PullFromQueueError.new("Bad uri error: #{e}")
|
105
|
+
rescue ArgumentError => e
|
106
|
+
raise PullFromQueueError.new("Argument error: #{e}")
|
107
|
+
rescue Crack::ParseError => e
|
108
|
+
raise PullFromQueueError.new("Parsing error: #{e}")
|
109
|
+
rescue Twitter::NotFound => e
|
110
|
+
raise PullFromQueueError.new("Account does not exist: #{e}")
|
111
|
+
rescue Twitter::Unauthorized => e
|
112
|
+
raise PullFromQueueError.new("Not authorized error: #{e}")
|
113
|
+
rescue Errno::ETIMEDOUT => e
|
114
|
+
raise PullFromQueueError.new("Connection timed out error: #{e}")
|
115
|
+
rescue Exception => e
|
116
|
+
HoptoadNotifier.notify(e)
|
117
|
+
raise PullFromQueueError.new("General error: #{e}")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Tweetable
|
2
|
+
class Search < Persistable
|
3
|
+
SEARCH_PER_PAGE_LIMIT = 100
|
4
|
+
SEARCH_START_PAGE = 1
|
5
|
+
|
6
|
+
attribute :created_at
|
7
|
+
attribute :updated_at
|
8
|
+
attribute :query
|
9
|
+
index :query
|
10
|
+
list :messages, Message
|
11
|
+
|
12
|
+
def update_all(force = false)
|
13
|
+
return unless needs_update?(force)
|
14
|
+
self.updated_at = Time.now.utc.to_s
|
15
|
+
self.save
|
16
|
+
update_messages
|
17
|
+
end
|
18
|
+
|
19
|
+
# Perform the search and update messages if any new exist
|
20
|
+
# Do up to 15 requests to collect as many historical messages as possible
|
21
|
+
# Because search API is different than the resto f the Twitter API this needs to be custom (i.e. cannot use xxx_from_timeline methods)
|
22
|
+
def update_messages(pages = 15)
|
23
|
+
most_recent_message = self.messages.first(:order => 'DESC', :by => :message_id)
|
24
|
+
since_id = most_recent_message.nil? ? 0 : most_recent_message.message_id
|
25
|
+
|
26
|
+
search_messages = []
|
27
|
+
pages.times do |page|
|
28
|
+
s = search(self.query, since_id, SEARCH_PER_PAGE_LIMIT, page+1)
|
29
|
+
break if s.results.nil?
|
30
|
+
search_messages += s.results
|
31
|
+
break if s.results.size < 99
|
32
|
+
end
|
33
|
+
|
34
|
+
search_messages.each do |message|
|
35
|
+
m = Message.find_or_create(:message_id, message.id)
|
36
|
+
|
37
|
+
m.update(
|
38
|
+
:message_id => message.id,
|
39
|
+
:favorited => message.favorited,
|
40
|
+
:photos_parsed => '0',
|
41
|
+
:links_parsed => '0',
|
42
|
+
:created_at => Time.now.utc.to_s,
|
43
|
+
:sent_at => message.created_at,
|
44
|
+
:text => message.text,
|
45
|
+
:from_screen_name => message.from_user) # we explicitly don't include the user_id provided by search since it's bullshit: http://code.google.com/p/twitter-api/issues/detail?id=214
|
46
|
+
|
47
|
+
next if !m.valid?
|
48
|
+
|
49
|
+
# create the user for this message
|
50
|
+
u = User.find_or_create(:screen_name, message.from_user)
|
51
|
+
u.update(
|
52
|
+
:user_id => message.from_user_id,
|
53
|
+
:profile_image_url => message.profile_image_url)
|
54
|
+
|
55
|
+
self.messages << m unless self.messages.include?(m)
|
56
|
+
end
|
57
|
+
|
58
|
+
search_messages.flatten
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def validate
|
65
|
+
assert_present :query
|
66
|
+
assert_unique :query
|
67
|
+
end
|
68
|
+
|
69
|
+
def search(query, since_id, per_page = SEARCH_PER_PAGE_LIMIT, page = SEARCH_START_PAGE)
|
70
|
+
begin
|
71
|
+
Twitter::Search.new(query.strip).since(since_id).per_page(per_page).page(page).fetch
|
72
|
+
rescue NoMethodError => e
|
73
|
+
raise TweetableError.new("Temporary problem searching Twitter: #{e}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Tweetable
|
2
|
+
class TwitterClient
|
3
|
+
attr_accessor :consumer_token, :consumer_secret, :authorization #:oauth_access_token, :oauth_access_secret
|
4
|
+
|
5
|
+
def method_missing(sym, *args, &block)
|
6
|
+
raise TweetableAuthError.new('Not authorized. You must login or authorize the client.') if @client.nil?
|
7
|
+
@client.send sym, *args, &block
|
8
|
+
end
|
9
|
+
|
10
|
+
def authorize(token, secret, authorization)
|
11
|
+
raise TweetableAuthError if authorization.nil?
|
12
|
+
|
13
|
+
self.authorization = authorization
|
14
|
+
self.consumer_token = token
|
15
|
+
self.consumer_secret = secret
|
16
|
+
self.oauth.authorize_from_access(self.authorization.oauth_access_token, self.authorization.oauth_access_secret)
|
17
|
+
|
18
|
+
@client = Twitter::Base.new(self.oauth)
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def login(username, password)
|
23
|
+
@client = Twitter::Base.new(Twitter::HTTPAuth.new(username, password))
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def oauth
|
28
|
+
@oauth ||= Twitter::OAuth.new(self.consumer_token, self.consumer_secret)
|
29
|
+
end
|
30
|
+
|
31
|
+
def status
|
32
|
+
status = self.rate_limit_status
|
33
|
+
{:hourly_limit => status[:hourly_limit], :remaining_hits => status[:remaining_hits], :reset_time => status[:reset_time], :reset_time_in_seconds => status[:reset_time_in_seconds]}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'tweetstream'
|
3
|
+
require 'daemons'
|
4
|
+
|
5
|
+
module Tweetable
|
6
|
+
class TwitterStreamingClient
|
7
|
+
|
8
|
+
def method_missing(sym, *args, &block)
|
9
|
+
@client.send sym, *args, &block
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(username, password, parser = nil)
|
13
|
+
@client = TweetStream::Client.new(username, password, parser)
|
14
|
+
setup
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(query_params)
|
19
|
+
query_params = query_params.call if query_params.kind_of?(Proc)
|
20
|
+
keywords = query_params[:track]
|
21
|
+
keywords = [keywords] unless keywords.kind_of?(Array)
|
22
|
+
|
23
|
+
Tweetable.log.debug("Tracking keywords: #{query_params.inspect}")
|
24
|
+
|
25
|
+
self.filter(query_params) do |status|
|
26
|
+
keywords.each do |keyword|
|
27
|
+
store(status, keyword)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def start(query_params = {}, daemon_options = {}) #:nodoc:
|
33
|
+
Daemons.run_proc('tweetable', daemon_options) do
|
34
|
+
Tweetable.log.debug("Starting...")
|
35
|
+
run(query_params)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def store(status, keyword)
|
40
|
+
if status.text =~ /#{keyword}/i
|
41
|
+
message = Message.create_from_timeline(status, true)
|
42
|
+
Tweetable.log.debug("[#{keyword}] #{message.sent_at} #{message.text} (#{message.message_id})")
|
43
|
+
|
44
|
+
search = Search.find_or_create(:query, keyword.downcase)
|
45
|
+
search.update(:updated_at => Time.now.utc.to_s)
|
46
|
+
search.messages << message
|
47
|
+
end
|
48
|
+
|
49
|
+
message
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def setup
|
54
|
+
self.on_delete do |status_id, user_id|
|
55
|
+
# do nothing
|
56
|
+
end
|
57
|
+
|
58
|
+
self.on_limit do |skip_count|
|
59
|
+
raise TweetableError.new("Twitter streaming rate limit reached (#{skip_count})")
|
60
|
+
end
|
61
|
+
|
62
|
+
self.on_error do |message|
|
63
|
+
puts "Error: #{message}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|