├── .gitignore ├── spec ├── fixtures │ ├── cat.jpg │ └── vcr_cassettes │ │ ├── petharbor.yml │ │ └── petfinder.yml ├── cute_pets_spec.rb ├── tweet_generator_spec.rb └── pet_fetcher_spec.rb ├── .travis.yml ├── civic.json ├── Gemfile ├── Rakefile ├── lib ├── greetings.yml ├── cute_pets.rb └── cute_pets │ ├── tweet_generator.rb │ └── pet_fetcher.rb ├── .env ├── app.json ├── Gemfile.lock ├── README.md └── where.geojson /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /spec/fixtures/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforamerica/CutePets/HEAD/spec/fixtures/cat.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - ruby-2.1.2 4 | notifications: 5 | webhooks: http://project-monitor.codeforamerica.org/projects/d702edae-5a33-478b-9d08-acc4a7826533/status 6 | -------------------------------------------------------------------------------- /civic.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "Beta", 3 | "tags": [ 4 | "shelter", 5 | "twitterbot", 6 | "easy to set up", 7 | "fellows", 8 | "2014", 9 | "heroku" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.1.2' 4 | 5 | gem 'twitter' 6 | gem 'dotenv' 7 | gem 'hpricot' 8 | gem 'rake' 9 | 10 | group :test do 11 | gem 'minitest' 12 | gem 'vcr' 13 | gem 'webmock' 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require './lib/cute_pets' 3 | 4 | Rake::TestTask.new do |t| 5 | t.pattern = 'spec/*_spec.rb' 6 | end 7 | 8 | desc 'Tweet random pet.' 9 | task :tweet_pet do 10 | CutePets.post_pet 11 | end 12 | 13 | task default: 'test' -------------------------------------------------------------------------------- /lib/greetings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - Hey! I'm 3 | - Hey there! I'm 4 | - Hi! My name is 5 | - Hello! I am 6 | - Hello! I'm 7 | - Hi! My name's 8 | - I'm 9 | - I am 10 | - They call me 11 | - Call me 12 | - I'm 13 | - Hello, I'm 14 | - Hey, I'm 15 | - Hi! I'm 16 | - Hey! I am 17 | -------------------------------------------------------------------------------- /lib/cute_pets.rb: -------------------------------------------------------------------------------- 1 | require './lib/cute_pets/pet_fetcher' 2 | require './lib/cute_pets/tweet_generator' 3 | require 'dotenv' 4 | Dotenv.load 5 | 6 | module CutePets 7 | extend self 8 | 9 | def post_pet() 10 | pet = nil 11 | if ENV.fetch('pet_datasource').downcase == 'petfinder' 12 | pet = PetFetcher.get_petfinder_pet 13 | else 14 | pet = PetFetcher.get_petharbor_pet 15 | end 16 | if pet 17 | message = TweetGenerator.create_message(pet[:name], pet[:description], pet[:link]) 18 | TweetGenerator.tweet(message, pet[:pic]) 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # NOTE. This is for local testing only. Use the heroku commands in the README to set these values on heroku. 2 | # For security reasons, never push your secret keys to github. 3 | 4 | # Twitter Keys 5 | api_key='your_api_key_goes_here' 6 | api_secret='your_api_secret_key_goes_here' 7 | access_token='your_access_token_goes_here' 8 | access_token_secret='your_access_token_secret_goes_here' 9 | 10 | # Pet Datasource Config 11 | # value is either 'petfinder' or 'petharbor' 12 | pet_datasource='petfinder' 13 | 14 | # Petfinder Keys 15 | # These must be set for data to be fetched from petfinder 16 | petfinder_key='your_petfinder_key_goes_here' 17 | petfinder_shelter_id='MA38' 18 | 19 | # Petharbor Key 20 | # This must be set for data to be fetched from Petharbor 21 | petharbor_shelter_id='DNVR' 22 | # This must be all or a smaller set of these pet types. 23 | # The types must be separated by a space. 24 | petharbor_pet_types='cat dog others' 25 | -------------------------------------------------------------------------------- /spec/cute_pets_spec.rb: -------------------------------------------------------------------------------- 1 | require 'cute_pets' 2 | require 'minitest/autorun' 3 | 4 | # Couldn't find a way to effectively mock modules via minitest :( 5 | describe 'CutePets' do 6 | describe '.post_pet' do 7 | before do 8 | @pet_hash = { name: 'schooples', 9 | link: 'http://www.example.com/schooples', 10 | pic: 'http://www.example.com/schooples.jpg', 11 | description: 'neutured female fluffy dog' 12 | } 13 | end 14 | it 'fetches pet finder data when the env var datasource is set to petfinder' do 15 | ENV.stub :fetch, 'petfinder' do 16 | PetFetcher.stub(:get_petfinder_pet, @pet_hash) do 17 | TweetGenerator.stub(:tweet, nil, [String, String]) do 18 | CutePets.post_pet 19 | end 20 | end 21 | end 22 | end 23 | 24 | it 'fetches pet harbor data when the env var datasource is set to petharbor' do 25 | ENV.stub :fetch, 'petharbor' do 26 | PetFetcher.stub(:get_petharbor_pet, @pet_hash) do 27 | TweetGenerator.stub(:tweet, nil, [String, String]) do 28 | CutePets.post_pet 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/tweet_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'cute_pets/tweet_generator' 2 | require 'minitest/autorun' 3 | 4 | describe 'TweetGenerator' do 5 | describe '.create_message' do 6 | it 'returns a string with the pet name, description and shelter link' do 7 | message = TweetGenerator.create_message('Moofle', 'neutered female lab', 'http://www.example.org/moofle') 8 | message.must_match /Moofle. I am a neutered female lab. http\:\/\/www\.example\.org\/moofle/ 9 | end 10 | 11 | it 'returns a string with the appropriate article' do 12 | message = TweetGenerator.create_message('Miffle', 'unaltered female kitten', 'http://www.example.org/miffle') 13 | message.must_match /Miffle. I am an unaltered female kitten. http\:\/\/www\.example\.org\/miffle/ 14 | end 15 | end 16 | 17 | describe '.tweet' do 18 | it 'uses the twitter gem to post a tweet' do 19 | client_object = MiniTest::Mock.new 20 | TweetGenerator.stub :client, client_object do 21 | message = 'Hi! I am Woofles. I am a neutered female nerdy cat. http://www.example.org/woofles' 22 | pet_pic = 'spec/fixtures/cat.jpg' 23 | client_object.expect(:update_with_media, nil, [String, File]) 24 | TweetGenerator.tweet(message, pet_pic) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CutePets", 3 | "description": "Twitter bot that posts adoptable pets in your city.", 4 | "repository": "https://github.com/hackyourcity-test/CutePets", 5 | "logo": "https://pbs.twimg.com/profile_images/491422568943857664/rKbENZuU.jpeg", 6 | "keywords": [ 7 | "codeforamerica" 8 | ], 9 | "env": { 10 | "api_key": { 11 | "description": "Your Twitter consumer key goes here" 12 | }, 13 | "api_secret": { 14 | "description": "Your Twitter consumer secret key goes here" 15 | }, 16 | "access_token": { 17 | "description": "Your Twitter access token goes here" 18 | }, 19 | "access_token_secret": { 20 | "description": "Your Twitter access token secret goes here" 21 | }, 22 | "petharbor_shelter_id": { 23 | "description": "The id of the pet harbpr shelter" 24 | }, 25 | "petharbor_pet_types": { 26 | "description": "The types of pets to search for ('dog', 'cat', or 'others', separated by spaces)", 27 | "value": "dog cat others" 28 | }, 29 | "pet_datasource": { 30 | "description": "The pet data source (don't change this)", 31 | "value": "petharbor" 32 | } 33 | }, 34 | "addons": [ 35 | "scheduler" 36 | ], 37 | "success_url" : "https://scheduler.heroku.com/dashboard" 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.6) 5 | buftok (0.2.0) 6 | crack (0.4.2) 7 | safe_yaml (~> 1.0.0) 8 | dotenv (0.11.1) 9 | dotenv-deployment (~> 0.0.2) 10 | dotenv-deployment (0.0.2) 11 | equalizer (0.0.9) 12 | faraday (0.9.0) 13 | multipart-post (>= 1.2, < 3) 14 | hpricot (0.8.6) 15 | http (0.6.2) 16 | http_parser.rb (~> 0.6.0) 17 | http_parser.rb (0.6.0) 18 | json (1.8.1) 19 | memoizable (0.4.2) 20 | thread_safe (~> 0.3, >= 0.3.1) 21 | minitest (5.4.1) 22 | multipart-post (2.0.0) 23 | naught (1.0.0) 24 | rake (10.3.2) 25 | safe_yaml (1.0.3) 26 | simple_oauth (0.2.0) 27 | thread_safe (0.3.4) 28 | twitter (5.11.0) 29 | addressable (~> 2.3) 30 | buftok (~> 0.2.0) 31 | equalizer (~> 0.0.9) 32 | faraday (~> 0.9.0) 33 | http (~> 0.6.0) 34 | http_parser.rb (~> 0.6.0) 35 | json (~> 1.8) 36 | memoizable (~> 0.4.0) 37 | naught (~> 1.0) 38 | simple_oauth (~> 0.2.0) 39 | vcr (2.9.3) 40 | webmock (1.18.0) 41 | addressable (>= 2.3.6) 42 | crack (>= 0.3.2) 43 | 44 | PLATFORMS 45 | ruby 46 | 47 | DEPENDENCIES 48 | dotenv 49 | hpricot 50 | minitest 51 | rake 52 | twitter 53 | vcr 54 | webmock 55 | -------------------------------------------------------------------------------- /lib/cute_pets/tweet_generator.rb: -------------------------------------------------------------------------------- 1 | require 'twitter' 2 | require 'open-uri' 3 | # This is terrible, but required. It resets a const in open-uri to guarantee that a File object 4 | # is always returned when open() is called. Without it, data less than 10k will cause open() to 5 | # return a StringIO object, which twitter can't handle. 6 | OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax') 7 | OpenURI::Buffer.const_set 'StringMax', 0 8 | 9 | require 'yaml' 10 | require 'dotenv' 11 | Dotenv.load 12 | 13 | module TweetGenerator 14 | extend self 15 | 16 | MESSAGES = YAML.load(File.open('lib/greetings.yml')) 17 | 18 | def tweet(message, pet_pic_url) 19 | pet_pic_img = open(pet_pic_url) 20 | client.update_with_media(message, pet_pic_img) 21 | end 22 | 23 | def create_message(pet_name, pet_description, pet_link) 24 | full_description = %w(a e i o u).include?(pet_description[0]) ? "an #{pet_description}" : "a #{pet_description}" 25 | "#{greeting} #{pet_name}. I am #{full_description}. #{pet_link}" 26 | end 27 | 28 | def greeting 29 | MESSAGES.sample 30 | end 31 | 32 | def client 33 | Twitter::REST::Client.new do |config| 34 | begin 35 | config.consumer_key = ENV.fetch('api_key') 36 | config.consumer_secret = ENV.fetch('api_secret') 37 | config.access_token = ENV.fetch('access_token') 38 | config.access_token_secret = ENV.fetch('access_token_secret') 39 | rescue KeyError 40 | raise "Please check that your twitter keys are correct" 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/petharbor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://www.petharbor.com/petoftheday.asp?availableonly=1&shelterlist='DNVR'&showstat=1&source=results&type=dog 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | Host: 17 | - www.petharbor.com 18 | response: 19 | status: 20 | code: 200 21 | message: OK 22 | headers: 23 | Cache-Control: 24 | - private 25 | Content-Length: 26 | - '1023' 27 | Content-Type: 28 | - text/html 29 | Server: 30 | - Microsoft-IIS/7.5 31 | Set-Cookie: 32 | - ASPSESSIONIDSQADQDSB=KLGMCKJDDBCPHPNOHPLHMGDF; path=/ 33 | X-Powered-By: 34 | - ARR/2.5 35 | - ASP.NET 36 | Date: 37 | - Mon, 15 Sep 2014 18:49:20 GMT 38 | body: 39 | encoding: UTF-8 40 | string: 'document.write ("
\"www.PetHarbor.com\"
MORTY (A223117)
ADOPTABLE
NEUTERED 49 | MALE WHITE BICHON FRISE
Denver Animal Shelter
"); ' 51 | http_version: 52 | recorded_at: Mon, 15 Sep 2014 18:49:28 GMT 53 | recorded_with: VCR 2.9.3 54 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/petfinder.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://api.petfinder.com/pet.getRandom?format=json&key=your_petfinder_key_goes_here&output=full&shelterid=MA38 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | Host: 17 | - api.petfinder.com 18 | response: 19 | status: 20 | code: 200 21 | message: OK 22 | headers: 23 | Content-Type: 24 | - application/json 25 | Date: 26 | - Fri, 12 Sep 2014 22:28:26 GMT 27 | Server: 28 | - Apache 29 | Content-Length: 30 | - '2135' 31 | Connection: 32 | - keep-alive 33 | body: 34 | encoding: UTF-8 35 | string: '{"@encoding":"iso-8859-1","@version":"1.0","petfinder":{"pet":{"options":{"option":{"$t":"altered"}},"breeds":{"breed":{"$t":"Ferret"}},"shelterPetId":{"$t":"A297612"},"status":{"$t":"A"},"name":{"$t":"JOEY"},"contact":{"email":{"$t":"adoption@mspca.org"},"zip":{"$t":"02130"},"city":{"$t":"Boston"},"fax":{"$t":"617-524-5652"},"address1":{"$t":"350 36 | South Huntington Avenue"},"phone":{"$t":"617-522-5055 "},"state":{"$t":"MA"},"address2":{}},"description":{"$t":"Patrick 37 | and Joey3 yearsNeutered malesMy owner did not have enough time for us and 38 | the new baby. Hi! We''re Patrick and Joey, two playful ferrets looking for 39 | a great home. We like ferret kibble and peanut butter treats, and we are 40 | litter box trained, so we are very neat. We are very energetic and mischievous. If 41 | you are looking for some fun new playmates, we are your guys!We are not picky 42 | about our housing - a basic ferret setup is fine for us, as long as we have 43 | enough room to move around. Take us home today!**We are already neutered, 44 | so we can go home with you right away!!**"},"sex":{"$t":"M"},"age":{"$t":"Adult"},"size":{"$t":"M"},"mix":{"$t":"no"},"shelterId":{"$t":"MA38"},"lastUpdate":{"$t":"2014-08-28T03:21:13Z"},"media":{"photos":{"photo":[{"@size":"pnt","$t":"http://photos.petfinder.com/photos/pets/30078059/1/?bust=1409196072&width=60&-pnt.jpg","@id":"1"},{"@size":"fpm","$t":"http://photos.petfinder.com/photos/pets/30078059/1/?bust=1409196072&width=95&-fpm.jpg","@id":"1"},{"@size":"x","$t":"http://photos.petfinder.com/photos/pets/30078059/1/?bust=1409196072&width=500&-x.jpg","@id":"1"},{"@size":"pn","$t":"http://photos.petfinder.com/photos/pets/30078059/1/?bust=1409196072&width=300&-pn.jpg","@id":"1"},{"@size":"t","$t":"http://photos.petfinder.com/photos/pets/30078059/1/?bust=1409196072&width=50&-t.jpg","@id":"1"}]}},"id":{"$t":"30078059"},"animal":{"$t":"Small 45 | & Furry"}},"@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance","header":{"timestamp":{"$t":"2014-09-12T22:28:26Z"},"status":{"message":{},"code":{"$t":"100"}},"version":{"$t":"0.1"}},"@xsi:noNamespaceSchemaLocation":"http://api.petfinder.com/schemas/0.9/petfinder.xsd"}}' 46 | http_version: 47 | recorded_at: Fri, 12 Sep 2014 22:28:27 GMT 48 | recorded_with: VCR 2.9.3 49 | -------------------------------------------------------------------------------- /spec/pet_fetcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'cute_pets/pet_fetcher' 2 | require 'minitest/autorun' 3 | require 'webmock/minitest' 4 | require 'vcr' 5 | 6 | describe 'PetFetcher' do 7 | VCR.configure do |c| 8 | c.cassette_library_dir = 'spec/fixtures/vcr_cassettes' 9 | c.hook_into :webmock 10 | end 11 | 12 | describe '.get_petfinder_pet' do 13 | it 'returns a hash of pet data when the API request is successful' do 14 | VCR.use_cassette('petfinder', record: :once) do 15 | pet_hash = PetFetcher.get_petfinder_pet 16 | pet_hash[:description].must_equal 'altered male ferret' 17 | pet_hash[:pic].must_equal 'http://photos.petfinder.com/photos/pets/30078059/1/?bust=1409196072&width=500&-x.jpg' 18 | pet_hash[:link].must_equal 'https://www.petfinder.com/petdetail/30078059' 19 | pet_hash[:name].must_equal 'Joey' 20 | end 21 | end 22 | 23 | it 'raises when the API request fails' do 24 | stub_request(:get, /^http\:\/\/api\.petfinder\.com\/pet\.getRandom/).to_return(:status => 500) 25 | lambda { PetFetcher.get_petfinder_pet }.must_raise RuntimeError 26 | end 27 | end 28 | 29 | describe '.get_petharbor_pet' do 30 | it 'returns a hash of pet data when the request is successful' do 31 | PetFetcher.stub(:get_petharbor_pet_type, 'dog') do 32 | VCR.use_cassette('petharbor', record: :once) do 33 | pet_hash = PetFetcher.get_petharbor_pet 34 | pet_hash[:description].must_equal 'neutered male white bichon frise' 35 | pet_hash[:pic].must_equal 'http://www.PetHarbor.com/get_image.asp?RES=Thumb&ID=A223117&LOCATION=DNVR' 36 | pet_hash[:link].must_equal 'http://www.PetHarbor.com/detail.asp?ID=A223117&LOCATION=DNVR&searchtype=rnd&shelterlist=\'DNVR\'&where=dummy&kiosk=1' 37 | pet_hash[:name].must_equal 'Morty' 38 | end 39 | end 40 | end 41 | end 42 | 43 | it 'raises when the request fails' do 44 | stub_request(:get, /^http\:\/\/www\.petharbor\.com\/petoftheday\.asp/).to_return(:status => 500) 45 | lambda { PetFetcher.get_petharbor_pet }.must_raise RuntimeError 46 | end 47 | 48 | describe 'get_petfinder_option' do 49 | it 'uses friendly values' do 50 | PetFetcher.send(:get_petfinder_option, {"option" => {"$t" => "housebroken"}}).must_equal 'house trained' 51 | PetFetcher.send(:get_petfinder_option, {"option" => {"$t" => "housetrained"}}).must_equal 'house trained' 52 | PetFetcher.send(:get_petfinder_option, {"option" => {"$t" => "noClaws"}}).must_equal 'declawed' 53 | PetFetcher.send(:get_petfinder_option, {"option" => {"$t" => "altered"}}).must_equal 'altered' 54 | end 55 | 56 | it 'handles multiple values in the options hash' do 57 | PetFetcher.send(:get_petfinder_option, 58 | {"option" => [{"$t" => "hasShots"}, 59 | {"$t" => "noClaws"}]}).must_equal 'declawed' 60 | end 61 | 62 | it 'ignores some possible values' do 63 | PetFetcher.send(:get_petfinder_option, 64 | {"option" => [{"$t" => "hasShots"}, 65 | {"$t" => "noCats"}, 66 | {"$t" => "noDogs"}, 67 | {"$t" => "noKids"}, 68 | {"$t" => "totally not in the xsd"}, 69 | ]}).must_equal nil 70 | 71 | end 72 | end 73 | 74 | describe 'get_petfinder_breed' do 75 | it 'works with a single hash' do 76 | PetFetcher.send(:get_petfinder_breed, {"breed" => {"$t" => "Spaniel"}}).must_equal 'Spaniel' 77 | end 78 | 79 | it 'works with an array of hashes' do 80 | PetFetcher.send(:get_petfinder_breed, {"breed" => [{"$t" => "Spaniel"}, {"$t" => "Pomeranian"}]}).must_equal 'Spaniel/Pomeranian mix' 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/cute_pets/pet_fetcher.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'json' 3 | require 'open-uri' 4 | require 'hpricot' 5 | require 'dotenv' 6 | Dotenv.load 7 | 8 | module PetFetcher 9 | extend self 10 | 11 | def get_petfinder_pet() 12 | uri = URI('http://api.petfinder.com/pet.getRandom') 13 | params = { 14 | format: 'json', 15 | key: ENV.fetch('petfinder_key'), 16 | shelterid: get_petfinder_shelter_id, 17 | output: 'full' 18 | } 19 | uri.query = URI.encode_www_form(params) 20 | response = Net::HTTP.get_response(uri) 21 | 22 | if response.kind_of? Net::HTTPSuccess 23 | json = JSON.parse(response.body) 24 | status_message = json['petfinder']['header']['status']['message']['$t'] 25 | if status_message == 'shelter opt-out' 26 | raise 'The chosen shelter opted out of being accesible via the API' 27 | elsif status_message == 'unauthorized key' 28 | raise 'Check that your Petfinder API key is configured correctly' 29 | elsif status_message 30 | raise status_message 31 | end 32 | pet_json = json['petfinder']['pet'] 33 | { 34 | pic: get_photo(pet_json), 35 | link: "https://www.petfinder.com/petdetail/#{pet_json['id']['$t']}", 36 | name: pet_json['name']['$t'].capitalize, 37 | description: [get_petfinder_option(pet_json['options']), get_petfinder_sex(pet_json['sex']['$t']), get_petfinder_breed(pet_json['breeds'])].compact.join(' ').downcase 38 | } 39 | else 40 | raise 'PetFinder api request failed' 41 | end 42 | end 43 | 44 | def get_petharbor_pet() 45 | uri = URI('http://www.petharbor.com/petoftheday.asp') 46 | 47 | params = { 48 | shelterlist: "\'#{get_petharbor_shelter_id}\'", 49 | type: get_petharbor_pet_type, 50 | availableonly: '1', 51 | showstat: '1', 52 | source: 'results' 53 | } 54 | uri.query = URI.encode_www_form(params) 55 | response = Net::HTTP.get_response(uri) 56 | if response.kind_of? Net::HTTPSuccess 57 | # The html response comes wrapped in some js :( 58 | response_html = response.body.gsub(/^document.write\s+\(\"/, '') 59 | response_html = response_html.gsub(/\"\);/, '') 60 | 61 | doc = Hpricot(response_html) 62 | pet_url = doc.at('//a').attributes['href'] 63 | pet_url = pet_url.gsub('\"', '').gsub('\\', '') 64 | pet_pic_html = doc.at('//a').inner_html 65 | pet_pic_url = pet_pic_html.match(/SRC=\\\"(?.+)\\\"\s+border/)['url'] 66 | table_cols = doc.search('//td') 67 | name = table_cols[1].inner_text.match(/^(?\w+)\s+/)['name'].capitalize 68 | description = table_cols[3].inner_text.downcase 69 | { 70 | pic: pet_pic_url, 71 | link: pet_url, 72 | name: name, 73 | description: description 74 | } 75 | else 76 | raise 'PetHarbor request failed' 77 | end 78 | end 79 | 80 | private 81 | 82 | def get_petfinder_sex(sex_abbreviation) 83 | sex_abbreviation.downcase == 'f' ? 'female' : 'male' 84 | end 85 | 86 | def get_petharbor_pet_type 87 | ENV.fetch('petharbor_pet_types').split.sample 88 | end 89 | 90 | PETFINDER_ADJECTIVES = { 91 | 'housebroken' => 'house trained', 92 | 'housetrained' => 'house trained', 93 | 'noClaws' => 'declawed', 94 | 'altered' => 'altered', 95 | 'noDogs' => nil, 96 | 'noCats' => nil, 97 | 'noKids' => nil, 98 | 'hasShots' => nil 99 | }.freeze 100 | 101 | def get_petfinder_option(option_hash) 102 | if option_hash['option'] 103 | [option_hash['option']].flatten.map { |hsh| PETFINDER_ADJECTIVES[hsh['$t']] }.compact.first 104 | else 105 | option_hash['$t'] 106 | end 107 | end 108 | 109 | def get_petfinder_breed(breeds) 110 | if breeds['breed'].is_a?(Array) 111 | "#{breeds['breed'].map(&:values).flatten.join('/')} mix" 112 | else 113 | breeds['breed']['$t'] 114 | end 115 | end 116 | 117 | def self.get_photo(pet) 118 | if !pet['media']['photos']['photo'].nil? 119 | pet['media']['photos']['photo'][2]['$t'] 120 | end 121 | end 122 | 123 | def get_petharbor_sex(html_text) 124 | html_text =~ /female/i ? 'female' : 'male' 125 | end 126 | 127 | def get_petfinder_shelter_id 128 | get_shelter_id(ENV.fetch('petfinder_shelter_id')) 129 | end 130 | 131 | def get_petharbor_shelter_id 132 | get_shelter_id(ENV.fetch('petharbor_shelter_id')) 133 | end 134 | 135 | def get_shelter_id(id) 136 | id.split(',').sample 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CutePets 2 | ======== 3 | 4 | Post a random adoptable pet from a shelter to a twitter feed 5 | ------------------------------------------------------------ 6 | 7 | Based off of Code for America's [CutePetsDenver](https://github.com/codeforamerica/cutepetsdenver) made by Team Denver. 8 | 9 | CutePetsDenver Twitter feed: [https://twitter.com/CutePetsDenver](https://twitter.com/CutePetsDenver) 10 | 11 | ![https://twitter.com/CutePetsDenver](http://i.imgur.com/TMKG80L.png) 12 | 13 | ## Get Your Own! 14 | We've tried to make this as simple as possible. You'll be setting up a few _free_ online accounts, connecting them together, then you'll get your very own CutePets twitter bot. Its painless and takes about an hour. Lets go. 15 | 16 | 17 | ### Instructions 18 | #### Summary: 19 | * We'll find an animal shelter we want to help get pets adopted from. 20 | * We'll set up a new Twitter account for these CutePets. 21 | * We'll create a bot that regularly posts the pets pictures and info. 22 | 23 | #### Requirements 24 | This service requires a credit card to get set up. This card will **NOT** be charged. 25 | 26 | #### Petharbor 27 | We're scraping info from Petharbor to make our Twitter bot. 28 | 29 | 1. Search on [petharbor.com](http://www.petharbor.com) for a shelter in your town. 30 | 2. Find the petharbor shelter id. The shelter id can be found near the end of the url, before the pet id, when clicking on the short link for a pet through petharbor.com. i.e. the shelter id in [http://www.petharbor.com/site.asp?ID=69155](http://www.petharbor.com/site.asp?ID=69155) is `69155`. 31 | 3. Figure out which pet types your shelter has. Do a search on [petharbor.com](http://www.petharbor.com) narrowed down by your shelter. It should be `cat` `dog` or `others`. 32 | 4. We'll need this info in a few minutes, keep it around. 33 | 34 | 35 | #### Twitter 36 | First we'll make a new twitter account, then we'll set up the 'bot' side of things. 37 | 38 | 1. Create a [twitter account](https://twitter.com/signup) with the user name you'd like to have stream your pet tweets 39 | 2. Add your phone number. If your phone number ia already attached to your personal Twitter account, you've got two options: 40 | * Quickest way is to make a new temporary phone number using [Google Voice](https://www.google.com/voice) or [BurnerApp](http://www.burnerapp.com/). 41 | * Create the new twitter app on your personal accountthen transfer the API key to the new bot account [here](https://support.twitter.com/forms/platform) by selecting “I need to transfer an API key to another account”, filling out the form, and waiting a few days. 42 | 3. Accept the confirmation email that Twitter sends you. 43 | 4. Create a new [twitter app](https://apps.twitter.com/). 44 | 5. For Website, use the new twitter account you just made, https://twitter.com/cutepetstester or whatever 45 | 6. Leave callback url blank 46 | 7. Accept Developer Agreement 47 | 8. Go to the Keys & Access Token tab 48 | 9. Create an Access Token 49 | 10. Good work so far. We're going to need all of these values soon, so keep this window open or write them down. 50 | 51 | #### Heroku 52 | This is where we'll turn on a free rented computer in the sky. It will run the code that grabs the info about animals from Petharbor, then tells Twitter to tweet about them. 53 | 54 | 1. Create a [Heroku account](https://id.heroku.com/signup/www-header) 55 | 2. Confirm the email they send you. 56 | 3. Cick here -> [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/codeforamerica/cutepets) 57 | 4. Give your new app a unique name. Try the same name as your Twitter account. 58 | 5. Enter in the Twitter values 59 | 6. Enter in the Petharbor values 60 | 7. Enter in your credit card info. You will **NOT** be charged. 61 | 8. Click on "Manage App" 62 | 9. Click on "Heroku Scheduler" 63 | 10. Click on "Add new job" 64 | 11. In the text box, copy and paste `rake tweet_pet` 65 | 12. Keep the dyno size at free. Set the frequency at daily. 66 | 13. Choose the closest time to now to send your first tweet. You can check UTC time at [https://www.google.com/search?q=utc+time](https://www.google.com/search?q=utc+time). 67 | 14. Wait for it, wait for it. 68 | 15. Yeah! You did it! 69 | 70 | #### Github 71 | Great work. Now, tell us which city you made a cutepets bot for. We do this by sending in a Pull Request with our twitter bot's name and location. 72 | 73 | 1. Make a new [Github Account](https://github.com/join). Choose the **free** account plan. 74 | 2. Check out the map on the [CutePets Repo](https://github.com/codeforamerica/CutePets/blob/master/where.geojson) 75 | 3. We want to add our own point to the map. We'll need the latitude and longitude for our city. Try using [Bing Maps](https://www.bing.com/maps/) or [http://www.latlong.net/](http://www.latlong.net/) to easily find them. 76 | 4. Edit the where.geojson file using [this link](https://github.com/codeforamerica/CutePets/edit/master/where.geojson). 77 | 5. Add in your twitter bot's name and location using the format below. Note that the negative longitude goes first. Be sure to have that comma at the very end too. 78 | ``` 79 | { 80 | "type": "Feature", 81 | "properties": 82 | { 83 | "twitter" : "http://twitter.com/CutePetsAdamsCo" 84 | }, 85 | "geometry": 86 | { 87 | "type": "Point", 88 | "coordinates": [ -104.871902 , 39.891651 ] 89 | } 90 | }, 91 | ``` 92 | 6. Click "Propose Changes" at the bottom of the page. 93 | 7. Click "Create Pull Request" 94 | 8. That's it! Thanks! 95 | -------------------------------------------------------------------------------- /where.geojson: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "type": "FeatureCollection", 4 | "features": [ 5 | 6 | { 7 | "type": "Feature", 8 | "properties": 9 | { 10 | "twitter" : "https://twitter.com/CutePetsLOLakes" 11 | }, 12 | "geometry": 13 | { 14 | "type": "Point", 15 | "coordinates": [ -82.457550, 28.223270 ] 16 | } 17 | }, 18 | { 19 | "type": "Feature", 20 | "properties": 21 | { 22 | "twitter" : "http://twitter.com/CutePetsAdamsCo" 23 | }, 24 | "geometry": 25 | { 26 | "type": "Point", 27 | "coordinates": [ -104.871902 , 39.891651 ] 28 | } 29 | }, 30 | { 31 | "type": "Feature", 32 | "properties": 33 | { 34 | "twitter" : "http://twitter.com/CutePetsCHA" 35 | }, 36 | "geometry": 37 | { 38 | "type": "Point", 39 | "coordinates": [ -85.249443 , 35.1067797 ] 40 | } 41 | }, 42 | { 43 | "type": "Feature", 44 | "properties": 45 | { 46 | "twitter" : "http://twitter.com/CuteRaleighPets" 47 | }, 48 | "geometry": 49 | { 50 | "type": "Point", 51 | "coordinates": [ -78.6294166, 35.8195622 ] 52 | } 53 | }, 54 | { 55 | "type": "Feature", 56 | "properties": 57 | { 58 | "twitter" : "https://twitter.com/CutiesInBoston" 59 | }, 60 | "geometry": 61 | { 62 | "type": "Point", 63 | "coordinates": [ -71.056740, 42.358662 ] 64 | } 65 | }, 66 | { 67 | "type": "Feature", 68 | "properties": 69 | { 70 | "twitter" : "https://twitter.com/CutePetsSF" 71 | }, 72 | "geometry": 73 | { 74 | "type": "Point", 75 | "coordinates": [ -122.419640, 37.777119 ] 76 | } 77 | }, 78 | { 79 | "type": "Feature", 80 | "properties": 81 | { 82 | "twitter" : "https://twitter.com/CuteBirdsInKC" 83 | }, 84 | "geometry": 85 | { 86 | "type": "Point", 87 | "coordinates": [ -94.583076, 39.102951 ] 88 | } 89 | }, 90 | { 91 | "type": "Feature", 92 | "properties": 93 | { 94 | "twitter" : "https://twitter.com/CuteRaleighPets" 95 | }, 96 | "geometry": 97 | { 98 | "type": "Point", 99 | "coordinates": [ -78.638889, 35.780556 ] 100 | } 101 | }, 102 | { 103 | "type": "Feature", 104 | "properties": 105 | { 106 | "twitter" : "https://twitter.com/CutePetsMSP" 107 | }, 108 | "geometry": 109 | { 110 | "type": "Point", 111 | "coordinates": [ -93.2667, 44.9833 ] 112 | } 113 | }, 114 | { 115 | "type": "Feature", 116 | "properties": 117 | { 118 | "twitter" : "https://twitter.com/CutePetsNorfolk" 119 | }, 120 | "geometry": 121 | { 122 | "type": "Point", 123 | "coordinates": [ -76.2000, 36.9167 ] 124 | } 125 | }, 126 | { 127 | "type": "Feature", 128 | "properties": 129 | { 130 | "twitter" : "https://twitter.com/CutePetsMesa" 131 | }, 132 | "geometry": 133 | { 134 | "type": "Point", 135 | "coordinates": [ -111.7377585, 33.3955 ] 136 | } 137 | }, 138 | { 139 | "type": "Feature", 140 | "properties": 141 | { 142 | "twitter" : "https://twitter.com/CutePetsSA" 143 | }, 144 | "geometry": 145 | { 146 | "type": "Point", 147 | "coordinates": [ -98.590635, 29.415142 ] 148 | } 149 | }, 150 | { 151 | "type": "Feature", 152 | "properties": 153 | { 154 | "twitter" : "https://twitter.com/CutePetsTucson" 155 | }, 156 | "geometry": 157 | { 158 | "type": "Point", 159 | "coordinates": [ -110.9264, 32.2217 ] 160 | } 161 | }, 162 | { 163 | "type": "Feature", 164 | "properties": 165 | { 166 | "twitter" : "https://twitter.com/CutePetsAnc" 167 | }, 168 | "geometry": 169 | { 170 | "type": "Point", 171 | "coordinates": [ -149.863129, 61.217381 ] 172 | } 173 | }, 174 | { 175 | "type": "Feature", 176 | "properties": 177 | { 178 | "twitter" : "https://twitter.com/cutepetsaustin" 179 | }, 180 | "geometry": 181 | { 182 | "type": "Point", 183 | "coordinates": [ -97.7534014, 30.3077609 ] 184 | } 185 | }, 186 | { 187 | "type": "Feature", 188 | "properties": 189 | { 190 | "twitter" : "https://twitter.com/CutePetsDenver" 191 | }, 192 | "geometry": 193 | { 194 | "type": "Point", 195 | "coordinates": [ -104.8551114, 39.7643389 ] 196 | } 197 | }, 198 | { 199 | "type": "Feature", 200 | "properties": 201 | { 202 | "twitter" : "https://twitter.com/CutePetsPtown" 203 | }, 204 | "geometry": 205 | { 206 | "type": "Point", 207 | "coordinates": [ -76.3558621, 36.8685605 ] 208 | } 209 | }, 210 | { 211 | "type": "Feature", 212 | "properties": 213 | { 214 | "twitter" : "https://twitter.com/indycutepets" 215 | }, 216 | "geometry": 217 | { 218 | "type": "Point", 219 | "coordinates": [ -86.153939, 39.768002 ] 220 | } 221 | }, 222 | { 223 | "type": "Feature", 224 | "properties": 225 | { 226 | "twitter" : "https://twitter.com/cutepetslou" 227 | }, 228 | "geometry": 229 | { 230 | "type": "Point", 231 | "coordinates": [ -85.7667, 38.2500 ] 232 | } 233 | }, 234 | { 235 | "type": "Feature", 236 | "properties": 237 | { 238 | "twitter" : "https://twitter.com/cutepetslex" 239 | }, 240 | "geometry": 241 | { 242 | "type": "Point", 243 | "coordinates": [ -84.494722, 38.029722 ] 244 | } 245 | }, 246 | { 247 | "type": "Feature", 248 | "properties": 249 | { 250 | "twitter" : "https://twitter.com/cutepetsnola" 251 | }, 252 | "geometry": 253 | { 254 | "type": "Point", 255 | "coordinates": [ -89.935135, 30.308489 ] 256 | } 257 | }, 258 | { 259 | "type": "Feature", 260 | "properties": 261 | { 262 | "twitter" : "https://twitter.com/cutepetsslc" 263 | }, 264 | "geometry": 265 | { 266 | "type": "Point", 267 | "coordinates": [ -111.8910500, 40.7607800 ] 268 | } 269 | }, 270 | { 271 | "type": "Feature", 272 | "properties": 273 | { 274 | "twitter" : "https://twitter.com/PetsToLove" 275 | }, 276 | "geometry": 277 | { 278 | "type": "Point", 279 | "coordinates": [ -80.2000, 25.7667 ] 280 | } 281 | }, 282 | { 283 | "type": "Feature", 284 | "properties": 285 | { 286 | "twitter" : "https://twitter.com/CutePetsAtlanta" 287 | }, 288 | "geometry": 289 | { 290 | "type": "Point", 291 | "coordinates": [ -84.3925952911377, 33.7453962306953 ] 292 | } 293 | }, 294 | { 295 | "type": "Feature", 296 | "properties": 297 | { 298 | "twitter" : "https://twitter.com/CutePetsBham" 299 | }, 300 | "geometry": 301 | { 302 | "type": "Point", 303 | "coordinates": [ -86.801728, 33.520648 ] 304 | } 305 | }, 306 | { 307 | "type": "Feature", 308 | "properties": 309 | { 310 | "twitter" : "https://twitter.com/petschemnitz" 311 | }, 312 | "geometry": 313 | { 314 | "type": "Point", 315 | "coordinates": [ 12.9252977, 50.8322608 ] 316 | } 317 | }, 318 | { 319 | "type": "Feature", 320 | "properties": 321 | { 322 | "twitter" : "https://twitter.com/CutePetsBmore" 323 | }, 324 | "geometry": 325 | { 326 | "type": "Point", 327 | "coordinates": [ -76.6167, 39.2833 ] 328 | } 329 | }, 330 | { 331 | "type": "Feature", 332 | "properties": 333 | { 334 | "twitter" : "https://twitter.com/CutePetsTtown" 335 | }, 336 | "geometry": 337 | { 338 | "type": "Point", 339 | "coordinates": [ -87.570555, 33.210795 ] 340 | } 341 | }, 342 | { 343 | "type": "Feature", 344 | "properties": 345 | { 346 | "twitter" : "https://twitter.com/CutePetsNYC" 347 | }, 348 | "geometry": 349 | { 350 | "type": "Point", 351 | "coordinates": [ -73.9604, 40.7684 ] 352 | } 353 | }, 354 | { 355 | "type": "Feature", 356 | "properties": 357 | { 358 | "twitter" : "https://twitter.com/CutePetsCologne" 359 | }, 360 | "geometry": 361 | { 362 | "type": "Point", 363 | "coordinates": [ 6.956944, 50.938056 ] 364 | } 365 | }, 366 | { 367 | "type": "Feature", 368 | "properties": 369 | { 370 | "twitter" : "https://twitter.com/CutePetsABQ" 371 | }, 372 | "geometry": 373 | { 374 | "type": "Point", 375 | "coordinates": [ -106.605553, 35.085334 ] 376 | } 377 | }, 378 | { 379 | "type": "Feature", 380 | "properties": 381 | { 382 | "twitter" : "https://twitter.com/CutePetsSTL" 383 | }, 384 | "geometry": 385 | { 386 | "type": "Point", 387 | "coordinates": [ -90.199404, 38.627003 ] 388 | } 389 | }, 390 | { 391 | "type": "Feature", 392 | "properties": 393 | { 394 | "twitter" : "https://twitter.com/PetsMinneapolis" 395 | }, 396 | "geometry": 397 | { 398 | "type": "Point", 399 | "coordinates": [ -93.265011 , 44.977753 ] 400 | } 401 | } 402 | ] 403 | } 404 | --------------------------------------------------------------------------------