DRMacIver-gourmand 0.0.3 → 0.0.9
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/.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
         |