lonely_coder 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +8 -0
- data/Gemfile.lock +58 -0
- data/README.mdown +72 -0
- data/lib/lonely_coder/authentication.rb +41 -0
- data/lib/lonely_coder/magic_constants.rb +99 -0
- data/lib/lonely_coder/profile.rb +99 -0
- data/lib/lonely_coder/search.rb +199 -0
- data/lib/lonely_coder/search_pagination_parser.rb +12 -0
- data/lib/lonely_coder.rb +38 -0
- data/spec/authentication_spec.rb +26 -0
- data/spec/cassettes/failed_authentication.yml +328 -0
- data/spec/cassettes/find_location.yml +44 -0
- data/spec/cassettes/paginate_search_results.yml +879 -0
- data/spec/cassettes/search_by_filters.yml +1619 -0
- data/spec/cassettes/search_by_username.yml +1234 -0
- data/spec/cassettes/successful_authentication.yml +871 -0
- data/spec/helper_spec.rb +8 -0
- data/spec/location_id_spec.rb +11 -0
- data/spec/pagination_spec.rb +18 -0
- data/spec/profile_spec.rb +175 -0
- data/spec/search_spec.rb +102 -0
- data/spec/spec_helper.rb +9 -0
- metadata +113 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
lonely_coder (0.1.0)
|
5
|
+
activesupport (>= 3.2.1)
|
6
|
+
mechanize (>= 2.0.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activesupport (3.2.2)
|
12
|
+
i18n (~> 0.6)
|
13
|
+
multi_json (~> 1.0)
|
14
|
+
addressable (2.2.7)
|
15
|
+
crack (0.3.1)
|
16
|
+
diff-lcs (1.1.3)
|
17
|
+
domain_name (0.5.2)
|
18
|
+
unf (~> 0.0.3)
|
19
|
+
i18n (0.6.0)
|
20
|
+
mechanize (2.3)
|
21
|
+
domain_name (~> 0.5, >= 0.5.1)
|
22
|
+
mime-types (~> 1.17, >= 1.17.2)
|
23
|
+
net-http-digest_auth (~> 1.1, >= 1.1.1)
|
24
|
+
net-http-persistent (~> 2.5, >= 2.5.2)
|
25
|
+
nokogiri (~> 1.4)
|
26
|
+
ntlm-http (~> 0.1, >= 0.1.1)
|
27
|
+
webrobots (~> 0.0, >= 0.0.9)
|
28
|
+
mime-types (1.18)
|
29
|
+
multi_json (1.1.0)
|
30
|
+
net-http-digest_auth (1.2)
|
31
|
+
net-http-persistent (2.5.2)
|
32
|
+
nokogiri (1.5.2)
|
33
|
+
ntlm-http (0.1.1)
|
34
|
+
rspec (2.9.0)
|
35
|
+
rspec-core (~> 2.9.0)
|
36
|
+
rspec-expectations (~> 2.9.0)
|
37
|
+
rspec-mocks (~> 2.9.0)
|
38
|
+
rspec-core (2.9.0)
|
39
|
+
rspec-expectations (2.9.0)
|
40
|
+
diff-lcs (~> 1.1.3)
|
41
|
+
rspec-mocks (2.9.0)
|
42
|
+
unf (0.0.5)
|
43
|
+
unf_ext
|
44
|
+
unf_ext (0.0.4)
|
45
|
+
vcr (2.0.0)
|
46
|
+
webmock (1.8.4)
|
47
|
+
addressable (>= 2.2.7)
|
48
|
+
crack (>= 0.1.7)
|
49
|
+
webrobots (0.0.13)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
lonely_coder!
|
56
|
+
rspec
|
57
|
+
vcr
|
58
|
+
webmock
|
data/README.mdown
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
Lonely Coder
|
2
|
+
===================
|
3
|
+
|
4
|
+
,d88b.d88b,
|
5
|
+
88888888888
|
6
|
+
`Y8888888Y'
|
7
|
+
`Y888Y'
|
8
|
+
`Y'
|
9
|
+
|
10
|
+
Lonely coder seeks nice boy
|
11
|
+
-----------------------------
|
12
|
+
require 'lonely_coder'
|
13
|
+
okc = OKCupid.new('thetrek','thisisntmypasswordweirdo')
|
14
|
+
|
15
|
+
sweet_guys = []
|
16
|
+
|
17
|
+
['Ann Arbor, MI', 'New York, New York',
|
18
|
+
'Chicago, Illinois', 'San Francisco, CA'].each do |maybe_here|
|
19
|
+
sweet_guys += okc.search({
|
20
|
+
:min_age => 25,
|
21
|
+
:max_age => 35, # just someone around my age, yo
|
22
|
+
:gentation => 'Guys who like guys', # don't hate
|
23
|
+
:order_by => 'Match %', # I want us to get along
|
24
|
+
:match_limit => 80, # let's make it last
|
25
|
+
:last_login => 'last month',
|
26
|
+
:location => maybe_here
|
27
|
+
:radius => 25, # acceptable values are 25, 50, 100, 250, 500, nil
|
28
|
+
:require_photo => true, # I like a goofy smile
|
29
|
+
:relationship_status => 'single' # the heart aches, but not for friends
|
30
|
+
}).results
|
31
|
+
end
|
32
|
+
|
33
|
+
sweet_guys.count # oof, 812. Let the coffee dates begin!
|
34
|
+
|
35
|
+
What is this nonsense?
|
36
|
+
-----------------------------
|
37
|
+
A ruby gem for interacting with [OKCupid](http://www.okcupid.com/). OKCupid doesn't actually have an API, so we use mechanize to interact with the site. Screen scraping isn't terribly fast, but it's pretty zippy compared to the other option – doing nothing.
|
38
|
+
|
39
|
+
How does it work?
|
40
|
+
-----------------------------
|
41
|
+
You'll need an OKCupid account (it's free) because most of the site is behind authentication. You can create a new connection to the site.
|
42
|
+
|
43
|
+
require 'lonely_coder'
|
44
|
+
okc = OKCupid.new('yourusername', 'yourpassword')
|
45
|
+
|
46
|
+
Once you have a connection you can check out a person's profile
|
47
|
+
|
48
|
+
trek = okc.profile_for('thetrek')
|
49
|
+
trek.match # => 99. Damn, he's pretty sweet
|
50
|
+
trek.location # => 'Ann Arbor, Michigan'
|
51
|
+
`open #{trek.profile_thumb_urls.first}` # HOT. DAMN.
|
52
|
+
|
53
|
+
Go ahead and ask for `username`, `match`, `friend`, `enemy`, `location`, `age`, `sex`, `orientation`, `single`, `last_online`, `ethnicity`, `height`, `body_type`, `diet`, `smokes`, `drinks`, `drugs`, `religion`, `sign`, `education`, `job`, `income`, `offspring`, `pets`, `speaks`, and `profile_thumb_urls`
|
54
|
+
|
55
|
+
If you don't have particular username in mind, you can search. Check `lib/magic_constants.rb` for the crazy [Magic Numbers](http://en.wikipedia.org/wiki/Magic_number_(programming\)).
|
56
|
+
|
57
|
+
search = okc.search({
|
58
|
+
:min_age => 18,
|
59
|
+
:max_age => 99,
|
60
|
+
:gentation => 'guys who like guys',
|
61
|
+
:ethnicity => ['human']
|
62
|
+
}) # search object
|
63
|
+
|
64
|
+
# fires the loading of the first 10, but only limited profiles. If you'd like full
|
65
|
+
# deets, use okc.profile_for with a results username.
|
66
|
+
search.results
|
67
|
+
search.load_next_page # loads the next 10 results
|
68
|
+
|
69
|
+
What if things don't go well
|
70
|
+
-----------------------------
|
71
|
+
|
72
|
+
okc.love(1000) # ♥
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class OKCupid
|
2
|
+
|
3
|
+
def authenticate(username, password)
|
4
|
+
@authentication = Authentication.new(username, password, @browser)
|
5
|
+
end
|
6
|
+
|
7
|
+
class Authentication
|
8
|
+
def initialize(username, password, browser)
|
9
|
+
change_to_using_simpler_parser(browser)
|
10
|
+
|
11
|
+
browser.post("https://www.okcupid.com/login", {
|
12
|
+
username: username,
|
13
|
+
password: password
|
14
|
+
})
|
15
|
+
|
16
|
+
@success = browser.page.uri.path == '/home'
|
17
|
+
|
18
|
+
restore_default_parser(browser)
|
19
|
+
end
|
20
|
+
|
21
|
+
def success?
|
22
|
+
@success
|
23
|
+
end
|
24
|
+
|
25
|
+
def change_to_using_simpler_parser(browser)
|
26
|
+
browser.pluggable_parser.html = AuthenticationParser
|
27
|
+
end
|
28
|
+
|
29
|
+
def restore_default_parser(browser)
|
30
|
+
browser.pluggable_parser.html = Mechanize::Page
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class AuthenticationParser < Mechanize::Page
|
35
|
+
# We're only using page uri to determine successful login, so
|
36
|
+
# there's not a lot of value in passing a body string to nokogiri
|
37
|
+
def initialize(uri = nil, response = nil, body = nil, code =nil)
|
38
|
+
super(uri, response, nil, code)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
class OKCupid
|
2
|
+
module MagicNumbers
|
3
|
+
# Used to build OKCupid search urls. These are the wacky values
|
4
|
+
# that OKCupid expects.
|
5
|
+
Ethnicity = {
|
6
|
+
"asian" => 2,
|
7
|
+
"black" => 8,
|
8
|
+
"hispanic/latin" => 128,
|
9
|
+
"indian" => 32,
|
10
|
+
"middle eastern" => 4,
|
11
|
+
"native american" => 16,
|
12
|
+
"pacific islander" => 64,
|
13
|
+
"white" => 256,
|
14
|
+
"human" => 512
|
15
|
+
}
|
16
|
+
|
17
|
+
Gentation = {
|
18
|
+
"girls who like guys" => 34,
|
19
|
+
"guys who like girls" => 17,
|
20
|
+
"girls who like girls" => 40,
|
21
|
+
"guys who like guys" => 20,
|
22
|
+
"both who like bi guys" => 54,
|
23
|
+
"both who like bi girls" => 57,
|
24
|
+
"straight girls only" => 2,
|
25
|
+
"Straight guys only" => 1,
|
26
|
+
"gay girls only" => 8,
|
27
|
+
"gay guys only" => 4,
|
28
|
+
"bi girls only" => 32,
|
29
|
+
"bi guys only" => 16,
|
30
|
+
"everybody" => 63
|
31
|
+
}
|
32
|
+
|
33
|
+
Filters = {
|
34
|
+
# "account_status" => 29,
|
35
|
+
"age" => 2,
|
36
|
+
# "body_type" => 30,
|
37
|
+
# "cats" => 17,
|
38
|
+
# "children" => 18,
|
39
|
+
# "community_award" => 31,
|
40
|
+
# "diet" => 54,
|
41
|
+
# "dogs" => 16,
|
42
|
+
# "drinking" => 12,
|
43
|
+
# "drugs" => 13,
|
44
|
+
# "education" => 19,
|
45
|
+
# "eligible" => 7,
|
46
|
+
"ethnicity" => 9,
|
47
|
+
"gentation" => 0,
|
48
|
+
# "height" => 10,
|
49
|
+
# "jobtype" => 15,
|
50
|
+
# "join_date" => 6,
|
51
|
+
# "languages" => 22,
|
52
|
+
"last_login" => 5,
|
53
|
+
# "looking_for" => 32,
|
54
|
+
# "money" => 14,
|
55
|
+
# "not_looking_for" => 34,
|
56
|
+
# "num_ques_ans" => 33,
|
57
|
+
# "personality" => 20,
|
58
|
+
# "prof_score" => 28,
|
59
|
+
"radius" => 3,
|
60
|
+
"relationship_status" => 35,
|
61
|
+
# "religion" => 8,
|
62
|
+
"require_photo" => 1,
|
63
|
+
# "sign" => 21,
|
64
|
+
# "smoking" => 11,
|
65
|
+
# "v_first_contact" => 27,
|
66
|
+
# "v_looks" => 23,
|
67
|
+
# "v_personality" => 25,
|
68
|
+
|
69
|
+
# added by us
|
70
|
+
'match_limit' => 'match_limit',
|
71
|
+
'order_by' => 'order_by',
|
72
|
+
'location' => 'location'
|
73
|
+
}
|
74
|
+
|
75
|
+
RelationshipStatus = {
|
76
|
+
'single' => 2,
|
77
|
+
'not single' => 12,
|
78
|
+
'any' => 0
|
79
|
+
}
|
80
|
+
|
81
|
+
OrderBy = {
|
82
|
+
'match %' => 'MATCH',
|
83
|
+
'friend %' => 'FRIEND',
|
84
|
+
'enemy %' => 'ENEMY',
|
85
|
+
'special blend' => 'SPECIAL_BLEND',
|
86
|
+
'join' => 'JOIN',
|
87
|
+
'last login' => 'LOGIN'
|
88
|
+
}
|
89
|
+
|
90
|
+
LastLogin = {
|
91
|
+
"now" => 3600,
|
92
|
+
"last day" => 86400,
|
93
|
+
"last week" => 604800,
|
94
|
+
"last month" => 2678400,
|
95
|
+
"last year" => 31536000,
|
96
|
+
"last decade" => 315360000
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
class OKCupid
|
3
|
+
|
4
|
+
def profile_for(username)
|
5
|
+
Profile.by_username(username, @browser)
|
6
|
+
end
|
7
|
+
|
8
|
+
class Profile
|
9
|
+
attr_accessor :username, :match, :friend, :enemy, :location,
|
10
|
+
:age, :sex, :orientation, :single, :small_avatar_url
|
11
|
+
|
12
|
+
# extended profile details
|
13
|
+
attr_accessor :last_online, :ethnicity, :height, :body_type, :diet, :smokes,
|
14
|
+
:drinks, :drugs, :religion, :sign, :education, :job, :income,
|
15
|
+
:offspring, :pets, :speaks, :profile_thumb_urls
|
16
|
+
|
17
|
+
|
18
|
+
# Scraping is never pretty.
|
19
|
+
def self.from_search_result(html)
|
20
|
+
|
21
|
+
username = html.search('span.username').text
|
22
|
+
age, sex, orientation, single = html.search('p.aso').text.split('/')
|
23
|
+
|
24
|
+
percents = html.search('div.percentages')
|
25
|
+
match = percents.search('p.match .percentage').text.to_i
|
26
|
+
friend = percents.search('p.friend .percentage').text.to_i
|
27
|
+
enemy = percents.search('p.enemy .percentage').text.to_i
|
28
|
+
|
29
|
+
location = html.search('p.location').text
|
30
|
+
small_avatar_url = html.search('a.user_image img').attribute('src').value
|
31
|
+
|
32
|
+
OKCupid::Profile.new({
|
33
|
+
username: username,
|
34
|
+
age: OKCupid.strip(age),
|
35
|
+
sex: OKCupid.strip(sex),
|
36
|
+
orientation: OKCupid.strip(orientation),
|
37
|
+
single: OKCupid.strip(single),
|
38
|
+
match: match,
|
39
|
+
friend: friend,
|
40
|
+
enemy: enemy,
|
41
|
+
location: location,
|
42
|
+
small_avatar_url: small_avatar_url
|
43
|
+
})
|
44
|
+
end
|
45
|
+
|
46
|
+
def Profile.by_username(username, browser)
|
47
|
+
html = browser.get("http://www.okcupid.com/profile/#{username}")
|
48
|
+
|
49
|
+
percents = html.search('#percentages')
|
50
|
+
match = percents.search('span.match').text.to_i
|
51
|
+
friend = percents.search('span.friend').text.to_i
|
52
|
+
enemy = percents.search('span.enemy').text.to_i
|
53
|
+
|
54
|
+
basic = html.search('#aso_loc')
|
55
|
+
age = basic.search('#ajax_age').text
|
56
|
+
sex = basic.search('#ajax_gender').text
|
57
|
+
orientation = basic.search('#ajax_orientation').text
|
58
|
+
single = basic.search('#ajax_status').text
|
59
|
+
location = basic.search('#ajax_location').text
|
60
|
+
|
61
|
+
profile_thumb_urls = html.search('#profile_thumbs img').collect {|img| img.attribute('src').value}
|
62
|
+
|
63
|
+
attributes = {
|
64
|
+
username: username,
|
65
|
+
match: match,
|
66
|
+
friend: friend,
|
67
|
+
enemy: enemy,
|
68
|
+
age: age,
|
69
|
+
sex: sex,
|
70
|
+
orientation: orientation,
|
71
|
+
location: location,
|
72
|
+
single: single,
|
73
|
+
profile_thumb_urls: profile_thumb_urls
|
74
|
+
}
|
75
|
+
|
76
|
+
details_div = html.search('#profile_details dl')
|
77
|
+
|
78
|
+
details_div.each do |node|
|
79
|
+
value = OKCupid.strip(node.search('dd').text)
|
80
|
+
next if value == '—'
|
81
|
+
|
82
|
+
attr_name = node.search('dt').text.downcase.gsub(' ','_')
|
83
|
+
attributes[attr_name] = value
|
84
|
+
end
|
85
|
+
|
86
|
+
self.new(attributes)
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(attributes)
|
90
|
+
attributes.each do |attr,val|
|
91
|
+
self.send("#{attr}=", val)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def ==(other)
|
96
|
+
self.username == other.username
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
class OKCupid
|
4
|
+
def search(options={})
|
5
|
+
Search.new(options, @browser)
|
6
|
+
end
|
7
|
+
|
8
|
+
class Search
|
9
|
+
class FilterError < StandardError; end
|
10
|
+
|
11
|
+
attr_reader :filters
|
12
|
+
|
13
|
+
def self.location_id_for(query)
|
14
|
+
uri = URI("http://www.okcupid.com/locquery?func=query&query=#{URI.encode(query)}")
|
15
|
+
JSON.parse(Net::HTTP.get(uri))['results'][0]['locid'].to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(options, browser = Mechanize.new)
|
19
|
+
@browser = browser
|
20
|
+
options = defaults.merge(options)
|
21
|
+
parse(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse(options)
|
25
|
+
combine_ages(options)
|
26
|
+
check_for_required_options(options)
|
27
|
+
remove_match_limit(options)
|
28
|
+
|
29
|
+
@filters = []
|
30
|
+
options.each do |name,value|
|
31
|
+
|
32
|
+
if OKCupid.const_defined?("#{name.to_s.camelize}Filter")
|
33
|
+
@filters << OKCupid.const_get("#{name.to_s.camelize}Filter").new(name, value)
|
34
|
+
else
|
35
|
+
@filters << Filter.new(name, value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def remove_match_limit(options)
|
41
|
+
@match_limit = options.delete(:match_limit)
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_for_required_options(options)
|
45
|
+
raise(FilterError, 'gentation is a required option') unless options.has_key?(:gentation)
|
46
|
+
end
|
47
|
+
|
48
|
+
def combine_ages(options)
|
49
|
+
age = [options.delete(:min_age), options.delete(:max_age)]
|
50
|
+
options[:age] = age
|
51
|
+
end
|
52
|
+
|
53
|
+
# Default values for search:
|
54
|
+
# match_limit 80
|
55
|
+
# min_age 18
|
56
|
+
# max_age 99
|
57
|
+
# order_by 'match %'
|
58
|
+
# last_login 'last month'
|
59
|
+
# location 'Near me'
|
60
|
+
# to search 'anywhere', use 'Near me' and omit a radius
|
61
|
+
# radius 25
|
62
|
+
# require_photo true
|
63
|
+
# relationship_status 'single'
|
64
|
+
def defaults
|
65
|
+
{
|
66
|
+
:match_limit => 80,
|
67
|
+
:min_age => 18,
|
68
|
+
:max_age => 99,
|
69
|
+
:order_by => 'Match %',
|
70
|
+
:last_login => 'last month',
|
71
|
+
:location => 'Near me',
|
72
|
+
:radius => 25, # acceptable values are 25, 50, 100, 250, 500, nil
|
73
|
+
:require_photo => true,
|
74
|
+
:relationship_status => 'single'
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def results
|
79
|
+
return @results if @results
|
80
|
+
|
81
|
+
page = @browser.get(url)
|
82
|
+
@results = page.search('.match_row').collect do |node|
|
83
|
+
OKCupid::Profile.from_search_result(node)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def load_next_page
|
88
|
+
@browser.pluggable_parser.html = SearchPaginationParser
|
89
|
+
page = @browser.get("#{url}&low=11&count=10&ajax_load=1")
|
90
|
+
@results += page.search('.match_row').collect do |node|
|
91
|
+
OKCupid::Profile.from_search_result(node)
|
92
|
+
end
|
93
|
+
@browser.pluggable_parser.html = Mechanize::Page
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def url
|
98
|
+
'/match?' + filters.compact.to_enum(:each_with_index).map {|filter,index| filter.to_param(index+1)}.join('&')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class Filter
|
103
|
+
class NoSuchFilter < StandardError; end
|
104
|
+
class BadValue < StandardError; end
|
105
|
+
|
106
|
+
attr_reader :name, :value, :code
|
107
|
+
|
108
|
+
def initialize(name, value)
|
109
|
+
@code = MagicNumbers::Filters[name.to_s]
|
110
|
+
raise(NoSuchFilter, name) unless @code
|
111
|
+
|
112
|
+
@name = name.to_s
|
113
|
+
@value = value
|
114
|
+
@encoded_value = lookup(@value)
|
115
|
+
unless @encoded_value
|
116
|
+
raise(BadValue, "#{@value.inspect} is not a possible value for #{@name}. Try one of #{allowed_values.map(&:inspect).join(', ')}")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def allowed_values
|
121
|
+
MagicNumbers.const_get(@name.camelize).keys
|
122
|
+
end
|
123
|
+
|
124
|
+
def lookup(value)
|
125
|
+
MagicNumbers.const_get(@name.camelize)[value.downcase]
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_param(n)
|
129
|
+
"filter#{n}=#{@code},#{@encoded_value}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class EthnicityFilter < Filter
|
134
|
+
def lookup(values)
|
135
|
+
# lookup the race values and sum them. I think OKC is doing some kind of base2 math on them
|
136
|
+
values.collect {|v| MagicNumbers::Ethnicity[v.downcase]}.inject(0, :+)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class LocationFilter < Filter
|
141
|
+
def lookup(value)
|
142
|
+
''
|
143
|
+
end
|
144
|
+
|
145
|
+
def to_param(n)
|
146
|
+
|
147
|
+
# to do: 'anywhere' needs to remove the radius filter
|
148
|
+
if @value.is_a?(String)
|
149
|
+
if @value.downcase == 'near me'
|
150
|
+
"locid=0"
|
151
|
+
else
|
152
|
+
"lquery=#{URI.escape(@value)}"
|
153
|
+
end
|
154
|
+
else
|
155
|
+
"locid=#{@value}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class OrderByFilter < Filter
|
161
|
+
def to_param(n)
|
162
|
+
"matchOrderBy=#{@encoded_value}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# we fake this by paginating results ourselves.
|
167
|
+
# class MatchLimitFilter < Filter
|
168
|
+
# def lookup(value)
|
169
|
+
# 'MATCH'
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# def to_param(n)
|
173
|
+
# nil
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
|
177
|
+
class RequirePhotoFilter < Filter
|
178
|
+
def lookup(value)
|
179
|
+
value ? 1 : 0
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class RadiusFilter < Filter
|
184
|
+
def lookup(value)
|
185
|
+
value.nil? ? '' : value
|
186
|
+
end
|
187
|
+
|
188
|
+
def to_param(n)
|
189
|
+
return nil if @encoded_value === ''
|
190
|
+
super
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class AgeFilter < Filter
|
195
|
+
def lookup(value)
|
196
|
+
"#{value[0]},#{value[1]}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'json'
|
2
|
+
class OKCupid
|
3
|
+
# OKCupid's ajax pagination follows pjax pattern and returns json
|
4
|
+
# with page fragments. We switch to this custom parser when
|
5
|
+
# interaction with search.
|
6
|
+
class SearchPaginationParser < Mechanize::Page
|
7
|
+
def initialize(uri = nil, response = nil, body = nil, code =nil)
|
8
|
+
body = JSON.parse(body)['html']
|
9
|
+
super(uri, response, body, code)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/lonely_coder.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#encoding: UTF-8
|
2
|
+
|
3
|
+
# Hey there.
|
4
|
+
# ,d88b.d88b,
|
5
|
+
# 88888888888
|
6
|
+
# `Y8888888Y'
|
7
|
+
# `Y888Y'
|
8
|
+
# `Y' - trek
|
9
|
+
#
|
10
|
+
require 'mechanize'
|
11
|
+
|
12
|
+
class OKCupid
|
13
|
+
BaseUrl = 'http://www.okcupid.com'
|
14
|
+
VERSION = '0.1.0'
|
15
|
+
|
16
|
+
def initialize(username=nil, password=nil)
|
17
|
+
@browser = Mechanize.new
|
18
|
+
@browser.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.79 Safari/535.11'
|
19
|
+
authenticate(username, password)
|
20
|
+
end
|
21
|
+
|
22
|
+
WhiteSpace = "\302\240"
|
23
|
+
def self.strip(str)
|
24
|
+
str.gsub(WhiteSpace, ' ').strip
|
25
|
+
end
|
26
|
+
|
27
|
+
def love(n=20)
|
28
|
+
' ♥ ' * n
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
require 'active_support/core_ext/string/inflections'
|
33
|
+
|
34
|
+
require 'lonely_coder/magic_constants'
|
35
|
+
require 'lonely_coder/profile'
|
36
|
+
require 'lonely_coder/search'
|
37
|
+
require 'lonely_coder/search_pagination_parser'
|
38
|
+
require 'lonely_coder/authentication'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Authentication' do
|
4
|
+
it "returns true if successful" do
|
5
|
+
VCR.use_cassette('successful_authentication', :erb => {username: ENV['OKC_USERNAME'], password: ENV['OKC_PASSWORD']}) do
|
6
|
+
auth = OKCupid::Authentication.new(ENV['OKC_USERNAME'], ENV['OKC_PASSWORD'], Mechanize.new)
|
7
|
+
auth.success?.should == true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
it "returns false if not successful" do
|
12
|
+
VCR.use_cassette('failed_authentication') do
|
13
|
+
auth = OKCupid::Authentication.new('thisisnotauser', 'thisisnotapassword', Mechanize.new)
|
14
|
+
auth.success?.should == false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
it "restores Mechanize::Page as a parser after authenticating" do
|
20
|
+
@browser = Mechanize.new
|
21
|
+
VCR.use_cassette('successful_authentication', :erb => {username: ENV['OKC_USERNAME'], password: ENV['OKC_PASSWORD']}) do
|
22
|
+
auth = OKCupid::Authentication.new(ENV['OKC_USERNAME'], ENV['OKC_PASSWORD'], @browser)
|
23
|
+
end
|
24
|
+
@browser.pluggable_parser['text/html'].should == Mechanize::Page
|
25
|
+
end
|
26
|
+
end
|