DRMacIver-gourmand 0.0.3 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Rakefile +7 -1
- data/VERSION +1 -1
- data/bin/gourmand +15 -6
- data/gourmand.gemspec +14 -5
- data/lib/gourmand.rb +84 -22
- data/lib/post.rb +17 -13
- data/lib/reddit.rb +4 -1
- data/lib/twitter.rb +41 -34
- data/lib/version.rb +14 -0
- data/spec/post_spec.rb +80 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/twitter_spec.rb +98 -0
- metadata +13 -6
- data/lib/blacklist.rb +0 -44
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg
|
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
+
require 'spec/rake/spectask'
|
3
4
|
|
4
5
|
require 'jeweler'
|
5
6
|
Jeweler::Tasks.new do |gem|
|
@@ -13,5 +14,10 @@ Jeweler::Tasks.new do |gem|
|
|
13
14
|
gem.add_dependency("json_pure")
|
14
15
|
gem.add_dependency("mechanize")
|
15
16
|
gem.add_dependency("httparty")
|
16
|
-
|
17
|
+
end
|
18
|
+
|
19
|
+
Spec::Rake::SpecTask.new do |t|
|
20
|
+
t.rcov = false
|
21
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
22
|
+
t.libs << "./lib"
|
17
23
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.9
|
data/bin/gourmand
CHANGED
@@ -9,7 +9,7 @@ def usage
|
|
9
9
|
gourmand update # Update a gourmand instance, posting the new bookmarks to delicious"
|
10
10
|
gourmand undo [site] # deletes all imported posts (from specific site, or all if not specified)
|
11
11
|
USAGE
|
12
|
-
|
12
|
+
Gourmand.site_names.each do |site|
|
13
13
|
STDERR.puts " gourmand #{site} # set your #{site} user"
|
14
14
|
end
|
15
15
|
|
@@ -17,11 +17,10 @@ def usage
|
|
17
17
|
end
|
18
18
|
|
19
19
|
dir=ENV["GOURMAND_HOME"] || File.join(ENV["HOME"], ".gourmand")
|
20
|
-
gourmand =
|
20
|
+
gourmand = Gourmand.new(dir)
|
21
21
|
|
22
|
-
if !
|
22
|
+
if !gourmand.exists?
|
23
23
|
puts "no such directory #{dir}. Creating..."
|
24
|
-
Dir.mkdir(dir)
|
25
24
|
|
26
25
|
puts "Delicious user name:"
|
27
26
|
delicious = STDIN.gets.strip
|
@@ -29,6 +28,16 @@ if !File.directory?(dir)
|
|
29
28
|
puts "Delicious password:"
|
30
29
|
delicious_password = STDIN.gets.strip
|
31
30
|
gourmand.create!(delicious, delicious_password)
|
31
|
+
elsif gourmand.needs_update?
|
32
|
+
puts "Your gourmand instance needs updating to use with this version. Do you want to update now? y/n"
|
33
|
+
if STDIN.gets =~ /y/i
|
34
|
+
gourmand.update!
|
35
|
+
else
|
36
|
+
exit(1)
|
37
|
+
end
|
38
|
+
elsif gourmand.from_the_future?
|
39
|
+
puts "This gourmand instance is more recent than the version of gourmand you are running. Please update in order to use it"
|
40
|
+
exit(1)
|
32
41
|
end
|
33
42
|
|
34
43
|
case ARGV[0]
|
@@ -39,10 +48,10 @@ case ARGV[0]
|
|
39
48
|
$stderr = log
|
40
49
|
gourmand.transfer_to_delicious
|
41
50
|
end
|
42
|
-
when *
|
51
|
+
when *Gourmand.site_names:
|
43
52
|
gourmand.site_for(ARGV[0]).ask_for_credentials
|
44
53
|
when "undo"
|
45
|
-
sites = ARGV[1..-1].empty? ?
|
54
|
+
sites = ARGV[1..-1].empty? ? Gourmand.site_names : ARGV[1..-1]
|
46
55
|
puts "undo import for sites #{sites.inspect}: are you sure? (y/n)"
|
47
56
|
if STDIN.gets.strip.downcase == 'y'
|
48
57
|
sites.each { |s| gourmand.site_for(s).undo_import! }
|
data/gourmand.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{gourmand}
|
5
|
-
s.version = "0.0.
|
5
|
+
s.version = "0.0.9"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["David R. MacIver"]
|
9
|
-
s.date = %q{2009-
|
9
|
+
s.date = %q{2009-08-15}
|
10
10
|
s.default_executable = %q{gourmand}
|
11
11
|
s.email = %q{david.maciver@gmail.com}
|
12
12
|
s.executables = ["gourmand"]
|
@@ -15,26 +15,35 @@ Gem::Specification.new do |s|
|
|
15
15
|
"README.markdown"
|
16
16
|
]
|
17
17
|
s.files = [
|
18
|
-
"
|
18
|
+
".gitignore",
|
19
|
+
"LICENSE",
|
19
20
|
"README.markdown",
|
20
21
|
"Rakefile",
|
21
22
|
"VERSION",
|
22
23
|
"bin/gourmand",
|
23
24
|
"gourmand.gemspec",
|
24
|
-
"lib/blacklist.rb",
|
25
25
|
"lib/delicious.rb",
|
26
26
|
"lib/gourmand.rb",
|
27
27
|
"lib/post.rb",
|
28
28
|
"lib/reddit.rb",
|
29
29
|
"lib/site.rb",
|
30
30
|
"lib/stumbleupon.rb",
|
31
|
-
"lib/twitter.rb"
|
31
|
+
"lib/twitter.rb",
|
32
|
+
"lib/version.rb",
|
33
|
+
"spec/post_spec.rb",
|
34
|
+
"spec/spec_helper.rb",
|
35
|
+
"spec/twitter_spec.rb"
|
32
36
|
]
|
33
37
|
s.homepage = %q{http://github.com/DRMacIver/gourmand}
|
34
38
|
s.rdoc_options = ["--charset=UTF-8"]
|
35
39
|
s.require_paths = ["lib"]
|
36
40
|
s.rubygems_version = %q{1.3.4}
|
37
41
|
s.summary = %q{gourmand is a tool for automatically importing links into delicious}
|
42
|
+
s.test_files = [
|
43
|
+
"spec/spec_helper.rb",
|
44
|
+
"spec/post_spec.rb",
|
45
|
+
"spec/twitter_spec.rb"
|
46
|
+
]
|
38
47
|
|
39
48
|
if s.respond_to? :specification_version then
|
40
49
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
data/lib/gourmand.rb
CHANGED
@@ -4,9 +4,43 @@ require "delicious"
|
|
4
4
|
require "json"
|
5
5
|
require 'net/http'
|
6
6
|
require 'uri'
|
7
|
-
require
|
7
|
+
require 'version'
|
8
|
+
require "fileutils"
|
9
|
+
|
10
|
+
class Gourmand
|
11
|
+
def self.version
|
12
|
+
Version.new(IO.read(File.join(File.dirname(__FILE__), "..", "VERSION")))
|
13
|
+
end
|
14
|
+
|
15
|
+
def version_file
|
16
|
+
File.join(@dir, "VERSION")
|
17
|
+
end
|
18
|
+
|
19
|
+
def version
|
20
|
+
if File.exists? version_file
|
21
|
+
Version.new(IO.read(version_file))
|
22
|
+
else
|
23
|
+
# the version right before this feature was added
|
24
|
+
Version.new("0.0.5")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def needs_update?
|
29
|
+
version < Gourmand.version
|
30
|
+
end
|
31
|
+
|
32
|
+
def from_the_future?
|
33
|
+
version > Gourmand.version
|
34
|
+
end
|
35
|
+
|
36
|
+
def update!
|
37
|
+
Migrations.new(self).migrate!
|
38
|
+
end
|
39
|
+
|
40
|
+
def exists?
|
41
|
+
File.exists?(@dir)
|
42
|
+
end
|
8
43
|
|
9
|
-
class Reddilicious
|
10
44
|
# lambdas to get lazy loading
|
11
45
|
SitesToClasses = {
|
12
46
|
"reddit" => lambda{
|
@@ -25,9 +59,8 @@ class Reddilicious
|
|
25
59
|
}
|
26
60
|
|
27
61
|
attr_accessor :dir
|
28
|
-
def initialize(dir)
|
62
|
+
def initialize(dir=File.join(ENV["HOME"], ".gourmand"))
|
29
63
|
@dir = dir
|
30
|
-
@blacklist = Blacklist.from_file(File.join(dir, "blacklist"))
|
31
64
|
@untiny_cache = if File.exists?(untiny_cache_file)
|
32
65
|
JSON.parse(IO.read(untiny_cache_file))
|
33
66
|
else
|
@@ -49,6 +82,8 @@ class Reddilicious
|
|
49
82
|
end
|
50
83
|
|
51
84
|
def create!(delicious, delicious_password)
|
85
|
+
Dir.mkdir(dir)
|
86
|
+
File.open(version_file, "w"){|o| o.puts Gourmand.version}
|
52
87
|
File.open(details_file, "w"){|o|
|
53
88
|
o.puts({:delicious_user => delicious, :delicious_password => delicious_password}.to_json)
|
54
89
|
}
|
@@ -86,11 +121,9 @@ class Reddilicious
|
|
86
121
|
end
|
87
122
|
end
|
88
123
|
|
89
|
-
def bookmark_for(url
|
124
|
+
def bookmark_for(url)
|
90
125
|
url = untiny_url(url)
|
91
|
-
Post.new
|
92
|
-
post.url = url
|
93
|
-
end
|
126
|
+
Post.new(:url=>url)
|
94
127
|
end
|
95
128
|
|
96
129
|
def delicious_posts
|
@@ -151,23 +184,16 @@ class Reddilicious
|
|
151
184
|
puts "#{new_updates.length} urls after merging"
|
152
185
|
|
153
186
|
new_updates.each do |update|
|
187
|
+
puts "importing #{update.description} (#{update.url})"
|
154
188
|
|
155
|
-
|
189
|
+
update.fetch_metadata!
|
156
190
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
puts "importing #{update.description} (#{update.url})"
|
161
|
-
|
162
|
-
update.fetch_metadata!(false)
|
163
|
-
|
164
|
-
res = Delicious.post("/posts/add", :query => update.to_h)
|
165
|
-
if !res['result'] || res['result']['code'] != 'done'
|
166
|
-
puts "error importing post: #{res.inspect}"
|
167
|
-
end
|
168
|
-
|
169
|
-
sleep(1)
|
191
|
+
res = Delicious.post("/posts/add", :query => update.to_h)
|
192
|
+
if !res['result'] || res['result']['code'] != 'done'
|
193
|
+
puts "error importing post: #{res.inspect}"
|
170
194
|
end
|
195
|
+
|
196
|
+
sleep(1)
|
171
197
|
end
|
172
198
|
puts "Saving data to storage"
|
173
199
|
sites.each{|x| x.save!}
|
@@ -193,3 +219,39 @@ class Reddilicious
|
|
193
219
|
end
|
194
220
|
|
195
221
|
end
|
222
|
+
|
223
|
+
class Migrations
|
224
|
+
Migrations = []
|
225
|
+
|
226
|
+
|
227
|
+
def initialize(gourmand)
|
228
|
+
@gourmand = gourmand
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.migration(version, &migration)
|
232
|
+
Migrations << [Version.new(version), migration]
|
233
|
+
end
|
234
|
+
|
235
|
+
migration("0.0.6"){|gourmand|
|
236
|
+
# This version does nothing except introduce version information
|
237
|
+
# into the gourmand directory. It is covered by the normal migration
|
238
|
+
# behaviour of adding a version file at the end.
|
239
|
+
}
|
240
|
+
|
241
|
+
migration("0.0.8"){|gourmand|
|
242
|
+
old_tweets = File.join(gourmand.dir, "twitter", "posts.json")
|
243
|
+
|
244
|
+
if File.exists? old_tweets
|
245
|
+
FileUtils.mv(old_tweets, File.join(gourmand.dir, "twitter", "friends.json"))
|
246
|
+
end
|
247
|
+
}
|
248
|
+
|
249
|
+
def migrate!
|
250
|
+
Migrations.
|
251
|
+
sort{|x, y| x[0] <=> y[0]}.
|
252
|
+
reject{|x| (x[0] <= @gourmand.version) || (x[0] > Gourmand.version)}.each{|version, mig|
|
253
|
+
mig.call(@gourmand)
|
254
|
+
}
|
255
|
+
File.open(@gourmand.version_file, "w"){|o| o.puts Gourmand.version}
|
256
|
+
end
|
257
|
+
end
|
data/lib/post.rb
CHANGED
@@ -15,7 +15,7 @@ class Post
|
|
15
15
|
yield self if block_given?
|
16
16
|
if hash
|
17
17
|
hash.each do |key, value|
|
18
|
-
instance_variable_set("@" + key, value)
|
18
|
+
instance_variable_set("@" + key.to_s, value)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
raise "all posts must have a URL" if !self.url
|
@@ -35,7 +35,7 @@ class Post
|
|
35
35
|
if !self.tags then Set.new else Set[*self.tags.split] end
|
36
36
|
end
|
37
37
|
|
38
|
-
def fetch_metadata!(
|
38
|
+
def fetch_metadata!()
|
39
39
|
self.description ||= begin
|
40
40
|
Nokogiri::HTML(open(url)).xpath("//title").text.gsub("\n", " ").gsub(/ +/, " ").strip
|
41
41
|
rescue Exception => e
|
@@ -44,19 +44,23 @@ class Post
|
|
44
44
|
end
|
45
45
|
|
46
46
|
self.description = url if description.empty?
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
47
|
+
end
|
48
|
+
|
49
|
+
def ==(that)
|
50
|
+
(that.is_a?(Post) &&
|
51
|
+
(self.url == that.url) &&
|
52
|
+
(self.description == that.description) &&
|
53
|
+
(self.extended == that.extended) &&
|
54
|
+
(self.dt == that.dt) &&
|
55
|
+
(self.tag_set == that.tag_set)) || false
|
56
|
+
end
|
57
|
+
|
58
|
+
def dup
|
59
|
+
Post.new(self.to_h)
|
56
60
|
end
|
57
61
|
|
58
62
|
def merge(that)
|
59
|
-
return self if
|
63
|
+
return self if that.nil?
|
60
64
|
raise "cannot merge posts with different URLS: #{self.url} != #{that.url}" if self.url != that.url
|
61
65
|
|
62
66
|
result = Post.new{|p|
|
@@ -84,7 +88,7 @@ class Post
|
|
84
88
|
p.dt = [self.dt, that.dt].compact.min
|
85
89
|
}
|
86
90
|
|
87
|
-
result = nil if result == that
|
91
|
+
result = nil if result == that
|
88
92
|
result
|
89
93
|
end
|
90
94
|
end
|
data/lib/reddit.rb
CHANGED
@@ -20,7 +20,10 @@ module Reddit
|
|
20
20
|
new_results = nil
|
21
21
|
after = nil
|
22
22
|
i = 0
|
23
|
-
|
23
|
+
|
24
|
+
url = "/user/#{(credentials["username"] || credentials["user"]).strip}/liked/.json"
|
25
|
+
puts "fetching data from #{url}"
|
26
|
+
while !(new_results = merge_results(Reddit.get(url, :query => {"after" => after})["data"]["children"].map{|x| x["data"]})).empty?
|
24
27
|
puts "fetching reddit page #{i}"
|
25
28
|
results += new_results
|
26
29
|
after = new_results[-1]["name"]
|
data/lib/twitter.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "httparty"
|
3
3
|
require "gourmand"
|
4
|
+
require "time"
|
4
5
|
|
5
6
|
module Twitter
|
6
7
|
include HTTParty
|
7
8
|
base_uri "http://twitter.com"
|
8
9
|
format :json
|
10
|
+
|
11
|
+
class Timeline < Site
|
12
|
+
def initialize(timeline)
|
13
|
+
@timeline = timeline
|
14
|
+
end
|
15
|
+
end
|
9
16
|
|
10
17
|
class FriendsTimeline < Site
|
11
18
|
def name
|
@@ -16,9 +23,7 @@ module Twitter
|
|
16
23
|
puts "Updating twitter"
|
17
24
|
balance
|
18
25
|
|
19
|
-
|
20
|
-
|
21
|
-
results = []
|
26
|
+
results = {}
|
22
27
|
|
23
28
|
new_tweets = nil
|
24
29
|
|
@@ -26,46 +31,49 @@ module Twitter
|
|
26
31
|
|
27
32
|
query = {:count => 200 }
|
28
33
|
|
29
|
-
query[:since_id] = last_post_id if last_post_id
|
30
|
-
|
31
34
|
while !(new_tweets = get_tweets(query)).empty?
|
32
|
-
|
35
|
+
new_tweets.reject!{|t| @ids.include? identifier(t) }
|
36
|
+
break if new_tweets.empty?
|
37
|
+
new_tweets.each { |t| results[identifier(t)] = t }
|
33
38
|
puts "importing twitter page #{query[:page] || 0}"
|
34
39
|
query[:page] = (query[:page] || 0) + 1
|
35
40
|
end
|
36
|
-
|
37
|
-
@posts += results
|
38
41
|
|
39
|
-
results.
|
40
|
-
urls = res["text"].scan(/(http:\/\/[^,()" ]+)/).flatten
|
41
|
-
ats = res["text"].scan(/@([[:alnum:]]+)/).flatten
|
42
|
-
hashtags = res["text"].scan(/#([[:alnum:]]+)/).flatten
|
43
|
-
retweet = res["text"] =~ /RT[^a-zA-Z]/ || res["text"] =~ /\(via @[^)]+\)/
|
42
|
+
@posts += results.values
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
post.tags = [
|
48
|
-
"via:twitter",
|
49
|
-
Post::NEW_MARKER,
|
50
|
-
ats.map{|a| "to:" + a}.sort,
|
51
|
-
"from:#{res["user"]["screen_name"]}",
|
52
|
-
hashtags,
|
53
|
-
("retweet" if retweet),
|
54
|
-
post.tags
|
55
|
-
].compact.flatten.join(" ").strip
|
56
|
-
|
57
|
-
post.extended = "@#{res["user"]["screen_name"]}: \"#{res["text"]}\" \n (from http://twitter.com/#{res["user"]["screen_name"]}/status/#{res["id"]})"
|
58
|
-
post.dt = date(res).strftime("%Y-%m-%dT%H:%M:%SZ")
|
59
|
-
|
60
|
-
post
|
61
|
-
end
|
44
|
+
results.values.map do |res|
|
45
|
+
to_posts(res)
|
62
46
|
end.flatten
|
63
47
|
end
|
48
|
+
|
49
|
+
def to_posts(res)
|
50
|
+
urls = res["text"].scan(/(http:\/\/[^,()" ]+)/).flatten.map { |u| u.gsub(/[\.:]+\Z/, '') }.uniq
|
51
|
+
ats = res["text"].scan(/@([[:alnum:]]+)/).flatten
|
52
|
+
hashtags = res["text"].scan(/#([[:alnum:]]+)/).flatten
|
53
|
+
retweet = res["text"] =~ /RT[^a-zA-Z]/ || res["text"] =~ /\(via @[^)]+\)/
|
64
54
|
|
55
|
+
urls.map do |url|
|
56
|
+
post = @gourmand.bookmark_for(url)
|
57
|
+
post.tags = [
|
58
|
+
"via:twitter",
|
59
|
+
Post::NEW_MARKER,
|
60
|
+
ats.map{|a| "to:" + a}.sort,
|
61
|
+
"from:#{res["user"]["screen_name"]}",
|
62
|
+
hashtags,
|
63
|
+
("retweet" if retweet),
|
64
|
+
post.tags
|
65
|
+
].compact.flatten.join(" ").strip
|
66
|
+
|
67
|
+
post.extended = "@#{res["user"]["screen_name"]}: \"#{res["text"]}\" \n (from http://twitter.com/#{res["user"]["screen_name"]}/status/#{res["id"]})"
|
68
|
+
post.dt = date(res).strftime("%Y-%m-%dT%H:%M:%SZ")
|
69
|
+
|
70
|
+
post
|
71
|
+
end
|
72
|
+
end
|
65
73
|
|
66
74
|
def get_tweets(query)
|
67
75
|
begin
|
68
|
-
res = Twitter.get("/
|
76
|
+
res = Twitter.get("/favorites.json", :query => query, :basic_auth => {:username => credentials["username"], :password => credentials["password"]})
|
69
77
|
raise "Error fetching timeline: '#{res['error']}'" if res.is_a?(Hash) && res['error']
|
70
78
|
res
|
71
79
|
rescue Crack::ParseError
|
@@ -73,16 +81,15 @@ module Twitter
|
|
73
81
|
end
|
74
82
|
end
|
75
83
|
|
76
|
-
|
77
84
|
def identifier(post)
|
78
85
|
post["id"]
|
79
86
|
end
|
80
87
|
|
81
88
|
def date(post)
|
82
|
-
|
89
|
+
@date_cache ||= Hash.new { |h,k| h[k] = Time.parse(k) }
|
90
|
+
@date_cache[post['created_at']]
|
83
91
|
end
|
84
92
|
|
85
|
-
|
86
93
|
def ask_for_credentials
|
87
94
|
puts "#{name} user name:"
|
88
95
|
user = STDIN.gets.strip
|
data/lib/version.rb
ADDED
data/spec/post_spec.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
require "post"
|
3
|
+
|
4
|
+
describe Post do
|
5
|
+
post = Post.new do |p|
|
6
|
+
p.url = "http://www.google.com"
|
7
|
+
p.description = "Google"
|
8
|
+
p.tags = "search"
|
9
|
+
end
|
10
|
+
|
11
|
+
post2 = Post.new do |p|
|
12
|
+
p.url = "http://www.google.com"
|
13
|
+
p.description = "Google"
|
14
|
+
p.tags = "search"
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
post3 = Post.new do |p|
|
19
|
+
p.url = "http://www.cuteoverload.com"
|
20
|
+
p.description = "Cute Overload"
|
21
|
+
p.tags = "cute kittens"
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "equality" do
|
25
|
+
it "should treat things with the same properties as equal" do
|
26
|
+
post.should == post
|
27
|
+
post.should == post2
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should treat things with different properties as unequal" do
|
31
|
+
post.should_not == post3
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should not be equal to nil" do
|
35
|
+
post.should_not == nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
describe "merging" do
|
41
|
+
later = post.dup
|
42
|
+
earlier = post.dup
|
43
|
+
|
44
|
+
later.dt = Time.now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
45
|
+
earlier.dt = (Time.now - 3600 * 24).strftime("%Y-%m-%dT%H:%M:%SZ")
|
46
|
+
|
47
|
+
|
48
|
+
it "should return nil when merging equal posts" do
|
49
|
+
post.merge(post2).should be(nil)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should return a post with the minimum time stamp of the two" do
|
53
|
+
z = earlier.merge(later)
|
54
|
+
|
55
|
+
z.should_not == nil
|
56
|
+
z.dt.should == earlier.dt
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should return self when merging with nil" do
|
60
|
+
post.merge(nil).should ==(post)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "dup" do
|
66
|
+
it "should produce an equal post" do
|
67
|
+
post.dup.should == post
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
it "should produce an independent post" do
|
72
|
+
p = post.dup
|
73
|
+
p.url = "http://www.google.co.uk"
|
74
|
+
|
75
|
+
post.url.should == "http://www.google.com"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
require "twitter"
|
3
|
+
require "tmpdir"
|
4
|
+
|
5
|
+
describe Twitter::FriendsTimeline do
|
6
|
+
|
7
|
+
before do
|
8
|
+
gourmand = mock('Gourmand')
|
9
|
+
gourmand.stub!(:dir).and_return(Dir::tmpdir)
|
10
|
+
gourmand.stub!(:bookmark_for).and_return { |url| Post.new :url=>url }
|
11
|
+
@twitter = Twitter::FriendsTimeline.new(gourmand)
|
12
|
+
@tweets = [{
|
13
|
+
'id' => '12345',
|
14
|
+
'text' => '@foo @bar kittens http://kittens.com http://morekittens.com http://kittens.com #cute #kittens',
|
15
|
+
'created_at' => '1984-09-01T14:21:31Z',
|
16
|
+
'user' => { 'screen_name' => 'baz', 'id' => 'baz_id' }
|
17
|
+
}, {
|
18
|
+
'id' => '12346',
|
19
|
+
'text'=> 'RT: check out this awesum link: http://awesome.com/awesome-story',
|
20
|
+
'created_at' => '1984-09-01T14:21:31Z',
|
21
|
+
'user' => { 'screen_name' => 'mongo', 'id' => 'mongo_id' }
|
22
|
+
}, {
|
23
|
+
'id' => '12347',
|
24
|
+
'text'=> 'this is a tweetie-style retweet: http://awesome.com/awesome-story (via @someone)',
|
25
|
+
'created_at' => '1984-09-01T14:21:31Z',
|
26
|
+
'user' => { 'screen_name' => 'mongo', 'id' => 'mongo_id' }
|
27
|
+
}, {
|
28
|
+
'id' => '12348',
|
29
|
+
'text'=> 'this is a tweet with punctuation after the link: http://bit.ly/das232...',
|
30
|
+
'created_at' => '1984-09-01T14:21:31Z',
|
31
|
+
'user' => { 'screen_name' => 'mongo', 'id' => 'mongo_id' }
|
32
|
+
}]
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
describe "to_posts" do
|
38
|
+
|
39
|
+
before do
|
40
|
+
@posts = @tweets.map { |t| @twitter.to_posts(t) }
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should convert a tweet to one or more posts, one for each unique url" do
|
44
|
+
@posts[0].size.should == 2
|
45
|
+
@posts[0].map { |p| p.url }.sort.should == ["http://kittens.com", "http://morekittens.com"]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should correctly set the timestamp" do
|
49
|
+
@posts[0][0].dt.should == '1984-09-01T14:21:31Z'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should convert hashtags into delicious tags" do
|
53
|
+
@posts[0][0].tag_set.should include('kittens')
|
54
|
+
@posts[0][0].tag_set.should include('cute')
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should add tags for all at's" do
|
58
|
+
['to:foo', 'to:bar'].each { |t| @posts[0][0].tag_set.should include(t) }
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should add tags for sender" do
|
62
|
+
@posts[0][0].tag_set.should include('from:baz')
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should add a retweet tag if it's a retweet" do
|
66
|
+
@posts[0][0].tag_set.should_not include('retweet')
|
67
|
+
@posts[1][0].tag_set.should include('retweet')
|
68
|
+
@posts[2][0].tag_set.should include('retweet')
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should deal with punctuation after urls" do
|
72
|
+
@posts[3][0].url.should == 'http://bit.ly/das232'
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should be tagged as new post by default" do
|
76
|
+
@posts.flatten.all? {|p| p.tag_set.should include(Post::NEW_MARKER) }.should be_true
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should be tagged as imported via twitter" do
|
80
|
+
@posts.flatten.all? {|p| p.tag_set.should include('via:twitter') }.should be_true
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should have an extended info with context" do
|
84
|
+
@posts[0][0].extended.should include(@tweets[0]['text'])
|
85
|
+
@posts[0][0].extended.should include("(from http://twitter.com/baz/status/12345)")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "update!" do
|
90
|
+
it "should ignore duplicates" do
|
91
|
+
@twitter.stub!(:get_tweets).and_return { |query|
|
92
|
+
query[:page].to_i > 0 ? [] : (@tweets * 2)
|
93
|
+
}
|
94
|
+
posts = @twitter.update!
|
95
|
+
posts.size.should ==@tweets.size + 1
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: DRMacIver-gourmand
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David R. MacIver
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-08-15 00:00:00 -07:00
|
13
13
|
default_executable: gourmand
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -62,13 +62,13 @@ extra_rdoc_files:
|
|
62
62
|
- LICENSE
|
63
63
|
- README.markdown
|
64
64
|
files:
|
65
|
+
- .gitignore
|
65
66
|
- LICENSE
|
66
67
|
- README.markdown
|
67
68
|
- Rakefile
|
68
69
|
- VERSION
|
69
70
|
- bin/gourmand
|
70
71
|
- gourmand.gemspec
|
71
|
-
- lib/blacklist.rb
|
72
72
|
- lib/delicious.rb
|
73
73
|
- lib/gourmand.rb
|
74
74
|
- lib/post.rb
|
@@ -76,8 +76,13 @@ files:
|
|
76
76
|
- lib/site.rb
|
77
77
|
- lib/stumbleupon.rb
|
78
78
|
- lib/twitter.rb
|
79
|
+
- lib/version.rb
|
80
|
+
- spec/post_spec.rb
|
81
|
+
- spec/spec_helper.rb
|
82
|
+
- spec/twitter_spec.rb
|
79
83
|
has_rdoc: false
|
80
84
|
homepage: http://github.com/DRMacIver/gourmand
|
85
|
+
licenses:
|
81
86
|
post_install_message:
|
82
87
|
rdoc_options:
|
83
88
|
- --charset=UTF-8
|
@@ -98,9 +103,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
103
|
requirements: []
|
99
104
|
|
100
105
|
rubyforge_project:
|
101
|
-
rubygems_version: 1.
|
106
|
+
rubygems_version: 1.3.5
|
102
107
|
signing_key:
|
103
108
|
specification_version: 3
|
104
109
|
summary: gourmand is a tool for automatically importing links into delicious
|
105
|
-
test_files:
|
106
|
-
|
110
|
+
test_files:
|
111
|
+
- spec/spec_helper.rb
|
112
|
+
- spec/post_spec.rb
|
113
|
+
- spec/twitter_spec.rb
|
data/lib/blacklist.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
class Blacklist
|
2
|
-
def initialize(blacklist)
|
3
|
-
@blacklist = Hash.new{|h, k| h[k] = [] }
|
4
|
-
|
5
|
-
blacklist.each { |list|
|
6
|
-
list.each { |tag|
|
7
|
-
@blacklist[tag] << list
|
8
|
-
}
|
9
|
-
}
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.from_file(file)
|
13
|
-
return nil if !File.exists?(file)
|
14
|
-
Blacklist.new(IO.read(file).split("\n").map{|l| l.split})
|
15
|
-
end
|
16
|
-
|
17
|
-
def blacklisted?(tags)
|
18
|
-
if tags.is_a? String
|
19
|
-
tags = tags.split
|
20
|
-
end
|
21
|
-
|
22
|
-
if !tags.is_a? Array
|
23
|
-
raise "unrecognised argument #{tags.inspect}"
|
24
|
-
end
|
25
|
-
|
26
|
-
shallow_flatten(tags.
|
27
|
-
map{|t| @blacklist[t]}.
|
28
|
-
compact).
|
29
|
-
any?{|set|
|
30
|
-
!set.empty? && set.all?{|t|
|
31
|
-
tags.include?(t)
|
32
|
-
}
|
33
|
-
}
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def shallow_flatten(enum)
|
39
|
-
it = []
|
40
|
-
enum.each{|x| it += x }
|
41
|
-
it
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|