├── .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 ("
 |
| 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 | 
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 -> [](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 |
--------------------------------------------------------------------------------