tweetable 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/Gemfile +8 -0
  2. data/History.txt +4 -0
  3. data/Manifest.txt +33 -0
  4. data/PostInstall.txt +7 -0
  5. data/README.rdoc +37 -0
  6. data/Rakefile +19 -0
  7. data/VERSION +1 -0
  8. data/lib/tweetable/authorization.rb +21 -0
  9. data/lib/tweetable/collection.rb +54 -0
  10. data/lib/tweetable/link.rb +30 -0
  11. data/lib/tweetable/message.rb +119 -0
  12. data/lib/tweetable/persistable.rb +32 -0
  13. data/lib/tweetable/photo.rb +6 -0
  14. data/lib/tweetable/queue.rb +121 -0
  15. data/lib/tweetable/search.rb +77 -0
  16. data/lib/tweetable/twitter_client.rb +36 -0
  17. data/lib/tweetable/twitter_streaming_client.rb +67 -0
  18. data/lib/tweetable/url.rb +39 -0
  19. data/lib/tweetable/user.rb +104 -0
  20. data/lib/tweetable.rb +82 -0
  21. data/pkg/tweetable-0.1.7.gem +0 -0
  22. data/script/console +10 -0
  23. data/script/destroy +14 -0
  24. data/script/generate +14 -0
  25. data/spec/collection_spec.rb +55 -0
  26. data/spec/fixtures/blank.json +1 -0
  27. data/spec/fixtures/flippyhead.json +1 -0
  28. data/spec/fixtures/follower_ids.json +1 -0
  29. data/spec/fixtures/friend_ids.json +1 -0
  30. data/spec/fixtures/friends_timeline.json +1 -0
  31. data/spec/fixtures/link_blank.json +1 -0
  32. data/spec/fixtures/link_exists.json +1 -0
  33. data/spec/fixtures/rate_limit_status.json +1 -0
  34. data/spec/fixtures/search.json +1 -0
  35. data/spec/fixtures/user_timeline.json +1 -0
  36. data/spec/fixtures/verify_credentials.json +1 -0
  37. data/spec/link_spec.rb +35 -0
  38. data/spec/message_spec.rb +148 -0
  39. data/spec/persistable_spec.rb +53 -0
  40. data/spec/queue_spec.rb +29 -0
  41. data/spec/search_spec.rb +60 -0
  42. data/spec/spec.opts +5 -0
  43. data/spec/spec_helper.rb +55 -0
  44. data/spec/tweetable_spec.rb +19 -0
  45. data/spec/twitter_client_spec.rb +41 -0
  46. data/spec/twitter_streaming_client_spec.rb +18 -0
  47. data/spec/user_spec.rb +143 -0
  48. data/tweetable.gemspec +106 -0
  49. metadata +165 -0
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source :gemcutter
2
+ gem 'logging'
3
+ gem 'twitter'
4
+
5
+
6
+ group :development do
7
+ gem 'rspec'
8
+ end
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2009-09-25
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
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
@@ -0,0 +1,7 @@
1
+
2
+ For more information on tweetable, see http://tweetable.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
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,6 @@
1
+ module Tweetable
2
+ class Photo < Link
3
+ attribute :created_at
4
+ attribute :updated_at
5
+ end
6
+ 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