dustin-twitter 0.3.2.1
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/History.txt +89 -0
- data/License.txt +19 -0
- data/Manifest.txt +66 -0
- data/README.txt +75 -0
- data/Rakefile +4 -0
- data/TODO.txt +2 -0
- data/bin/twitter +15 -0
- data/config/hoe.rb +74 -0
- data/config/requirements.rb +17 -0
- data/examples/blocks.rb +15 -0
- data/examples/direct_messages.rb +26 -0
- data/examples/favorites.rb +20 -0
- data/examples/friends_followers.rb +25 -0
- data/examples/friendships.rb +13 -0
- data/examples/location.rb +8 -0
- data/examples/replies.rb +26 -0
- data/examples/sent_messages.rb +26 -0
- data/examples/timeline.rb +33 -0
- data/examples/twitter.rb +27 -0
- data/examples/verify_credentials.rb +13 -0
- data/lib/twitter/base.rb +248 -0
- data/lib/twitter/cli/config.rb +9 -0
- data/lib/twitter/cli/helpers.rb +97 -0
- data/lib/twitter/cli/migrations/20080722194500_create_accounts.rb +13 -0
- data/lib/twitter/cli/migrations/20080722194508_create_tweets.rb +16 -0
- data/lib/twitter/cli/migrations/20080722214605_add_account_id_to_tweets.rb +9 -0
- data/lib/twitter/cli/migrations/20080722214606_create_configurations.rb +13 -0
- data/lib/twitter/cli/models/account.rb +33 -0
- data/lib/twitter/cli/models/configuration.rb +13 -0
- data/lib/twitter/cli/models/tweet.rb +20 -0
- data/lib/twitter/cli.rb +328 -0
- data/lib/twitter/direct_message.rb +22 -0
- data/lib/twitter/easy_class_maker.rb +43 -0
- data/lib/twitter/rate_limit_status.rb +19 -0
- data/lib/twitter/status.rb +22 -0
- data/lib/twitter/user.rb +37 -0
- data/lib/twitter/version.rb +9 -0
- data/lib/twitter.rb +20 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +74 -0
- data/setup.rb +1585 -0
- data/spec/base_spec.rb +109 -0
- data/spec/cli/helper_spec.rb +35 -0
- data/spec/direct_message_spec.rb +35 -0
- data/spec/fixtures/followers.xml +706 -0
- data/spec/fixtures/friends.xml +609 -0
- data/spec/fixtures/friends_for.xml +584 -0
- data/spec/fixtures/friends_lite.xml +192 -0
- data/spec/fixtures/friends_timeline.xml +66 -0
- data/spec/fixtures/public_timeline.xml +148 -0
- data/spec/fixtures/rate_limit_status.xml +7 -0
- data/spec/fixtures/status.xml +25 -0
- data/spec/fixtures/user.xml +38 -0
- data/spec/fixtures/user_timeline.xml +465 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/status_spec.rb +40 -0
- data/spec/user_spec.rb +42 -0
- data/tasks/deployment.rake +41 -0
- data/tasks/environment.rake +7 -0
- data/tasks/website.rake +17 -0
- data/twitter.gemspec +49 -0
- data/website/css/common.css +47 -0
- data/website/images/terminal_output.png +0 -0
- data/website/index.html +138 -0
- metadata +176 -0
data/lib/twitter/base.rb
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
# This is the base class for the twitter library. It makes all the requests
|
2
|
+
# to twitter, parses the xml (using hpricot) and returns ruby objects to play with.
|
3
|
+
#
|
4
|
+
# For complete documentation on the options, check out the twitter api docs.
|
5
|
+
# http://groups.google.com/group/twitter-development-talk/web/api-documentation
|
6
|
+
module Twitter
|
7
|
+
class Base
|
8
|
+
|
9
|
+
# Initializes the configuration for making requests to twitter
|
10
|
+
def initialize(email, password, host='twitter.com')
|
11
|
+
@config, @config[:email], @config[:password] = {}, email, password
|
12
|
+
@api_host = host
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns an array of statuses for a timeline; Defaults to your friends timeline.
|
16
|
+
def timeline(which=:friends, options={})
|
17
|
+
raise UnknownTimeline unless [:friends, :public, :user].include?(which)
|
18
|
+
auth = which.to_s.include?('public') ? false : true
|
19
|
+
statuses(call("#{which}_timeline", :auth => auth, :since => options[:since], :args => parse_options(options)))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns an array of users who are in your friends list
|
23
|
+
def friends(options={})
|
24
|
+
users(call(:friends, {:args => parse_options(options)}))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns an array of users who are friends for the id or username passed in
|
28
|
+
def friends_for(id, options={})
|
29
|
+
friends(options.merge({:id => id}))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns an array of users who are following you
|
33
|
+
def followers(options={})
|
34
|
+
users(call(:followers, {:args => parse_options(options)}))
|
35
|
+
end
|
36
|
+
|
37
|
+
def followers_for(id, options={})
|
38
|
+
followers(options.merge({:id => id}))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a single status for a given id
|
42
|
+
def status(id)
|
43
|
+
statuses(call("show/#{id}")).first
|
44
|
+
end
|
45
|
+
|
46
|
+
# returns all the profile information and the last status for a user
|
47
|
+
def user(id_or_screenname)
|
48
|
+
users(request("users/show/#{id_or_screenname}.xml", :auth => true)).first
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns an array of statuses that are replies
|
52
|
+
def replies(options={})
|
53
|
+
statuses(call(:replies, :since => options[:since], :args => parse_options(options)))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Destroys a status by id
|
57
|
+
def destroy(id)
|
58
|
+
call("destroy/#{id}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def rate_limit_status
|
62
|
+
RateLimitStatus.new_from_xml request("account/rate_limit_status.xml", :auth => true)
|
63
|
+
end
|
64
|
+
|
65
|
+
# waiting for twitter to correctly implement this in the api as it is documented
|
66
|
+
def featured
|
67
|
+
users(call(:featured))
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns an array of all the direct messages for the authenticated user
|
71
|
+
def direct_messages(options={})
|
72
|
+
doc = request(build_path('direct_messages.xml', parse_options(options)), {:auth => true, :since => options[:since]})
|
73
|
+
(doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
|
74
|
+
end
|
75
|
+
alias :received_messages :direct_messages
|
76
|
+
|
77
|
+
# Returns direct messages sent by auth user
|
78
|
+
def sent_messages(options={})
|
79
|
+
doc = request(build_path('direct_messages/sent.xml', parse_options(options)), {:auth => true, :since => options[:since]})
|
80
|
+
(doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
|
81
|
+
end
|
82
|
+
|
83
|
+
# destroys a give direct message by id if the auth user is a recipient
|
84
|
+
def destroy_direct_message(id)
|
85
|
+
request("direct_messages/destroy/#{id}.xml", :auth => true)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Sends a direct message <code>text</code> to <code>user</code>
|
89
|
+
def d(user, text)
|
90
|
+
url = URI.parse("http://#{@api_host}/direct_messages/new.xml")
|
91
|
+
req = Net::HTTP::Post.new(url.path)
|
92
|
+
req.basic_auth(@config[:email], @config[:password])
|
93
|
+
req.set_form_data({'text' => text, 'user' => user})
|
94
|
+
response = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) }
|
95
|
+
DirectMessage.new_from_xml(parse(response.body).at('direct_message'))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Befriends id_or_screenname for the auth user
|
99
|
+
def create_friendship(id_or_screenname)
|
100
|
+
users(request("friendships/create/#{id_or_screenname}.xml", :auth => true)).first
|
101
|
+
end
|
102
|
+
|
103
|
+
# Defriends id_or_screenname for the auth user
|
104
|
+
def destroy_friendship(id_or_screenname)
|
105
|
+
users(request("friendships/destroy/#{id_or_screenname}.xml", :auth => true)).first
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns true if friendship exists, false if it doesn't.
|
109
|
+
def friendship_exists?(user_a, user_b)
|
110
|
+
doc = request(build_path("friendships/exists.xml", {:user_a => user_a, :user_b => user_b}), :auth => true)
|
111
|
+
doc.at('friends').innerHTML == 'true' ? true : false
|
112
|
+
end
|
113
|
+
|
114
|
+
# Updates your location and returns Twitter::User object
|
115
|
+
def update_location(location)
|
116
|
+
users(request(build_path('account/update_location.xml', {'location' => location}), :auth => true)).first
|
117
|
+
end
|
118
|
+
|
119
|
+
# Updates your deliver device and returns Twitter::User object
|
120
|
+
def update_delivery_device(device)
|
121
|
+
users(request(build_path('account/update_delivery_device.xml', {'device' => device}), :auth => true)).first
|
122
|
+
end
|
123
|
+
|
124
|
+
# Turns notifications by id_or_screenname on for auth user.
|
125
|
+
def follow(id_or_screenname)
|
126
|
+
users(request("notifications/follow/#{id_or_screenname}.xml", :auth => true)).first
|
127
|
+
end
|
128
|
+
|
129
|
+
# Turns notifications by id_or_screenname off for auth user.
|
130
|
+
def leave(id_or_screenname)
|
131
|
+
users(request("notifications/leave/#{id_or_screenname}.xml", :auth => true)).first
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns the most recent favorite statuses for the autenticating user
|
135
|
+
def favorites(options={})
|
136
|
+
statuses(request(build_path('favorites.xml', parse_options(options)), :auth => true))
|
137
|
+
end
|
138
|
+
|
139
|
+
# Favorites the status specified by id for the auth user
|
140
|
+
def create_favorite(id)
|
141
|
+
statuses(request("favorites/create/#{id}.xml", :auth => true)).first
|
142
|
+
end
|
143
|
+
|
144
|
+
# Un-favorites the status specified by id for the auth user
|
145
|
+
def destroy_favorite(id)
|
146
|
+
statuses(request("favorites/destroy/#{id}.xml", :auth => true)).first
|
147
|
+
end
|
148
|
+
|
149
|
+
# Blocks the user specified by id for the auth user
|
150
|
+
def block(id)
|
151
|
+
users(request("blocks/create/#{id}.xml", :auth => true)).first
|
152
|
+
end
|
153
|
+
|
154
|
+
# Unblocks the user specified by id for the auth user
|
155
|
+
def unblock(id)
|
156
|
+
users(request("blocks/destroy/#{id}.xml", :auth => true)).first
|
157
|
+
end
|
158
|
+
|
159
|
+
# Posts a new update to twitter for auth user.
|
160
|
+
def post(status, options={})
|
161
|
+
form_data = {'status' => status}
|
162
|
+
form_data.merge({'source' => options[:source]}) if options[:source]
|
163
|
+
url = URI.parse("http://#{@api_host}/statuses/update.xml")
|
164
|
+
req = Net::HTTP::Post.new(url.path)
|
165
|
+
req.basic_auth(@config[:email], @config[:password])
|
166
|
+
req.set_form_data(form_data)
|
167
|
+
response = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) }
|
168
|
+
Status.new_from_xml(parse(response.body).at('status'))
|
169
|
+
end
|
170
|
+
alias :update :post
|
171
|
+
|
172
|
+
# Verifies the credentials for the auth user.
|
173
|
+
# raises Twitter::CantConnect on failure.
|
174
|
+
def verify_credentials
|
175
|
+
request('account/verify_credentials', :auth => true)
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
# Converts an hpricot doc to an array of statuses
|
180
|
+
def statuses(doc)
|
181
|
+
(doc/:status).inject([]) { |statuses, status| statuses << Status.new_from_xml(status); statuses }
|
182
|
+
end
|
183
|
+
|
184
|
+
# Converts an hpricot doc to an array of users
|
185
|
+
def users(doc)
|
186
|
+
(doc/:user).inject([]) { |users, user| users << User.new_from_xml(user); users }
|
187
|
+
end
|
188
|
+
|
189
|
+
# Calls whatever api method requested that deals with statuses
|
190
|
+
#
|
191
|
+
# ie: call(:public_timeline, :auth => false)
|
192
|
+
def call(method, options={})
|
193
|
+
options.reverse_merge!({ :auth => true, :args => {} })
|
194
|
+
# Following line needed as lite=false doesn't work in the API: http://tinyurl.com/yo3h5d
|
195
|
+
options[:args].delete(:lite) unless options[:args][:lite]
|
196
|
+
args = options.delete(:args)
|
197
|
+
request(build_path("statuses/#{method.to_s}.xml", args), options)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Makes a request to twitter.
|
201
|
+
def request(path, options={})
|
202
|
+
options.reverse_merge!({:headers => { "User-Agent" => @config[:email] }})
|
203
|
+
unless options[:since].blank?
|
204
|
+
since = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s
|
205
|
+
options[:headers]["If-Modified-Since"] = since
|
206
|
+
end
|
207
|
+
|
208
|
+
begin
|
209
|
+
response = Net::HTTP.start(@api_host, 80) do |http|
|
210
|
+
req = Net::HTTP::Get.new('/' + path, options[:headers])
|
211
|
+
req.basic_auth(@config[:email], @config[:password]) if options[:auth]
|
212
|
+
http.request(req)
|
213
|
+
end
|
214
|
+
rescue => error
|
215
|
+
raise CantConnect, error.message
|
216
|
+
end
|
217
|
+
|
218
|
+
if %w[200 304].include?(response.code)
|
219
|
+
response = parse(response.body)
|
220
|
+
raise RateExceeded if (response/:hash/:error).text =~ /Rate limit exceeded/
|
221
|
+
response
|
222
|
+
elsif response.code == '503'
|
223
|
+
raise Unavailable, response.message
|
224
|
+
elsif response.code == '401'
|
225
|
+
raise CantConnect, 'Authentication failed. Check your username and password'
|
226
|
+
else
|
227
|
+
raise CantConnect, "Twitter is returning a #{response.code}: #{response.message}"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Given a path and a hash, build a full path with the hash turned into a query string
|
232
|
+
def build_path(path, options)
|
233
|
+
path += "?#{options.to_query}" unless options.blank?
|
234
|
+
path
|
235
|
+
end
|
236
|
+
|
237
|
+
# Tries to get all the options in the correct format before making the request
|
238
|
+
def parse_options(options)
|
239
|
+
options[:since] = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s if options[:since]
|
240
|
+
options
|
241
|
+
end
|
242
|
+
|
243
|
+
# Converts a string response into an Hpricot xml element.
|
244
|
+
def parse(response)
|
245
|
+
Hpricot.XML(response || '')
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Twitter
|
2
|
+
module CLI
|
3
|
+
module Helpers
|
4
|
+
class NoActiveAccount < StandardError; end
|
5
|
+
class NoAccounts < StandardError; end
|
6
|
+
|
7
|
+
def output_tweets(collection, options={})
|
8
|
+
options.reverse_merge!({
|
9
|
+
:cache => false,
|
10
|
+
:since_prefix => '',
|
11
|
+
:empty_msg => 'Nothing new since your last check.'
|
12
|
+
})
|
13
|
+
if collection.size > 0
|
14
|
+
justify = collection.collect { |s| s.user.screen_name }.max { |a,b| a.length <=> b.length }.length rescue 0
|
15
|
+
indention = ' ' * (justify + 3)
|
16
|
+
say("\n#{indention}#{collection.size} new tweet(s) found.\n\n")
|
17
|
+
collection.each do |s|
|
18
|
+
Tweet.create_from_tweet(current_account, s) if options[:cache]
|
19
|
+
occurred_at = Time.parse(s.created_at).strftime('On %b %d at %l:%M%P')
|
20
|
+
formatted_time = '-' * occurred_at.length + "\n#{indention}#{occurred_at}"
|
21
|
+
formatted_name = s.user.screen_name.rjust(justify + 1)
|
22
|
+
formatted_msg = ''
|
23
|
+
s.text.split(' ').in_groups_of(6, false) { |row| formatted_msg += row.join(' ') + "\n#{indention}" }
|
24
|
+
say "#{CGI::unescapeHTML(formatted_name)}: #{CGI::unescapeHTML(formatted_msg)}#{formatted_time}\n\n"
|
25
|
+
end
|
26
|
+
Configuration["#{options[:since_prefix]}_since_id"] = collection.first.id
|
27
|
+
else
|
28
|
+
say(options[:empty_msg])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def base(username=current_account.username, password=current_account.password)
|
33
|
+
@base ||= Twitter::Base.new(username, password)
|
34
|
+
end
|
35
|
+
|
36
|
+
def current_account
|
37
|
+
@current_account ||= Account.active
|
38
|
+
raise Account.count == 0 ? NoAccounts : NoActiveAccount if @current_account.blank?
|
39
|
+
@current_account
|
40
|
+
end
|
41
|
+
|
42
|
+
def attempt_import(&block)
|
43
|
+
tweet_file = File.join(ENV['HOME'], '.twitter')
|
44
|
+
if File.exists?(tweet_file)
|
45
|
+
say '.twitter file found, attempting import...'
|
46
|
+
config = YAML::load(File.read(tweet_file))
|
47
|
+
if !config['email'].blank? && !config['password'].blank?
|
48
|
+
Account.add(:username => config['email'], :password => config['password'])
|
49
|
+
say 'Account imported'
|
50
|
+
block.call if block_given?
|
51
|
+
true
|
52
|
+
else
|
53
|
+
say "Either your username or password were blank in your .twitter file so I could not import. Use 'twitter add' to add an account."
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def do_work(&block)
|
60
|
+
connect
|
61
|
+
begin
|
62
|
+
block.call
|
63
|
+
rescue Twitter::RateExceeded
|
64
|
+
say("Twitter says you've been making too many requests. Wait for a bit and try again.")
|
65
|
+
rescue Twitter::Unavailable
|
66
|
+
say("Twitter is unavailable right now. Try again later.")
|
67
|
+
rescue Twitter::CantConnect => msg
|
68
|
+
say("Can't connect to twitter because: #{msg}")
|
69
|
+
rescue Twitter::CLI::Helpers::NoActiveAccount
|
70
|
+
say("You have not set an active account. Use 'twitter change' to set one now.")
|
71
|
+
rescue Twitter::CLI::Helpers::NoAccounts
|
72
|
+
unless attempt_import { block.call }
|
73
|
+
say("You have not created any accounts. Use 'twitter add' to create one now.")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def connect
|
79
|
+
ActiveRecord::Base.logger = Logger.new('/tmp/twitter_ar_logger.log')
|
80
|
+
ActiveRecord::Base.establish_connection(Twitter::CLI::Config)
|
81
|
+
ActiveRecord::Base.connection
|
82
|
+
end
|
83
|
+
|
84
|
+
def migrate
|
85
|
+
connect
|
86
|
+
ActiveRecord::Migrator.migrate("#{CLI_ROOT}/migrations/")
|
87
|
+
end
|
88
|
+
|
89
|
+
def connect_and_migrate
|
90
|
+
say('Attempting to establish connection...')
|
91
|
+
connect
|
92
|
+
say('Connection established...migrating database...')
|
93
|
+
migrate
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateTweets < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :tweets do |t|
|
4
|
+
t.datetime :occurred_at
|
5
|
+
t.boolean :truncated, :favorited, :user_protected, :default => false
|
6
|
+
t.integer :twitter_id, :user_id, :in_reply_to_status_id, :in_reply_to_user_id, :user_followers_count
|
7
|
+
t.text :body
|
8
|
+
t.string :source, :user_name, :user_screen_name, :user_location, :user_description, :user_profile_image_url, :user_url
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.down
|
14
|
+
drop_table :tweets
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Account < ActiveRecord::Base
|
2
|
+
named_scope :current, :conditions => {:current => true}
|
3
|
+
|
4
|
+
has_many :tweets, :dependent => :destroy
|
5
|
+
|
6
|
+
def self.add(hash)
|
7
|
+
username = hash.delete(:username)
|
8
|
+
account = find_or_initialize_by_username(username)
|
9
|
+
account.attributes = hash
|
10
|
+
account.save
|
11
|
+
set_current(account) if new_active_needed?
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.active
|
15
|
+
current.first
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.set_current(account_or_id)
|
19
|
+
account = account_or_id.is_a?(Account) ? account_or_id : find(account_or_id)
|
20
|
+
account.update_attribute :current, true
|
21
|
+
Account.update_all "current = 0", "id != #{account.id}"
|
22
|
+
account
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.new_active_needed?
|
26
|
+
self.current.count == 0 && self.count > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"#{current? ? '*' : ' '} #{username}"
|
31
|
+
end
|
32
|
+
alias to_str to_s
|
33
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Configuration < ActiveRecord::Base
|
2
|
+
serialize :data
|
3
|
+
|
4
|
+
def self.[](key)
|
5
|
+
key = find_by_key(key.to_s)
|
6
|
+
key.nil? ? nil : key.data
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.[]=(key, data)
|
10
|
+
c = find_or_create_by_key(key.to_s)
|
11
|
+
c.update_attribute :data, data
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Tweet < ActiveRecord::Base
|
2
|
+
belongs_to :account
|
3
|
+
|
4
|
+
def self.create_from_tweet(account, s)
|
5
|
+
tweet = account.tweets.find_or_initialize_by_twitter_id(s.id)
|
6
|
+
tweet.body = s.text
|
7
|
+
tweet.occurred_at = s.created_at
|
8
|
+
|
9
|
+
%w[truncated favorited in_reply_to_status_id in_reply_to_user_id source].each do |m|
|
10
|
+
tweet.send("#{m}=", s.send(m))
|
11
|
+
end
|
12
|
+
|
13
|
+
%w[id followers_count name screen_name location description
|
14
|
+
profile_image_url url protected].each do |m|
|
15
|
+
tweet.send("user_#{m}=", s.user.send(m))
|
16
|
+
end
|
17
|
+
tweet.save!
|
18
|
+
tweet
|
19
|
+
end
|
20
|
+
end
|