lonely_coder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|