geocaching 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/README.markdown +40 -0
- data/Rakefile +9 -0
- data/geocaching.gemspec +21 -0
- data/lib/geocaching.rb +66 -0
- data/lib/geocaching/cache.rb +356 -0
- data/lib/geocaching/http.rb +219 -0
- data/lib/geocaching/log.rb +168 -0
- data/spec/cache_spec.rb +74 -0
- data/spec/helper.rb +12 -0
- data/spec/log_message.txt +5 -0
- data/spec/log_spec.rb +23 -0
- metadata +77 -0
data/README.markdown
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Ruby library for accessing geocaching.com information
|
2
|
+
=====================================================
|
3
|
+
|
4
|
+
This Ruby library provides access to cache and log information
|
5
|
+
on geocaching.com. It works by parsing the website’s content as
|
6
|
+
Groundspeak does not offer an API yet.
|
7
|
+
|
8
|
+
|
9
|
+
Example
|
10
|
+
-------
|
11
|
+
|
12
|
+
require "geocaching"
|
13
|
+
|
14
|
+
Geocaching::HTTP.login("username", "password")
|
15
|
+
|
16
|
+
cache = Geocaching::Cache.fetch(:code => "GCF00")
|
17
|
+
log = Geocaching::Log.fetch(:guid => "...")
|
18
|
+
|
19
|
+
puts cache.name #=> "Bridge Over Troubled Waters"
|
20
|
+
puts cache.difficulty #=> 2.5
|
21
|
+
puts cache.logs.size #=> 194
|
22
|
+
|
23
|
+
puts log.username #=> "Chris"
|
24
|
+
puts log.message #=> "TFTC ..."
|
25
|
+
puts log.cache #=> #<Geocaching::Cache:...>
|
26
|
+
|
27
|
+
Geocaching::HTTP.logout
|
28
|
+
|
29
|
+
Altough some cache information are available without being logged in,
|
30
|
+
most information will only be accessible after a successful login.
|
31
|
+
|
32
|
+
|
33
|
+
Tests
|
34
|
+
-----
|
35
|
+
|
36
|
+
Tests are written using RSpec.
|
37
|
+
|
38
|
+
$ export GC_USERNAME="username"
|
39
|
+
$ export GC_PASSWORD="password"
|
40
|
+
$ rake test
|
data/Rakefile
ADDED
data/geocaching.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "geocaching"
|
3
|
+
s.version = "0.1.0"
|
4
|
+
|
5
|
+
s.summary = "Library for accessing information on geocaching.com"
|
6
|
+
s.description = <<-EOD
|
7
|
+
A Ruby library that allows to access information on geocaching.com
|
8
|
+
in an object-oriented manner.
|
9
|
+
EOD
|
10
|
+
|
11
|
+
s.author = "nano"
|
12
|
+
s.email = "nano@fooo.org"
|
13
|
+
s.homepage = "http://nano.github.com/geocaching"
|
14
|
+
|
15
|
+
s.has_rdoc = false
|
16
|
+
s.has_yardoc = true
|
17
|
+
|
18
|
+
s.files = %w(README.markdown Rakefile geocaching.gemspec)
|
19
|
+
s.files += Dir.glob("lib/**/*")
|
20
|
+
s.files += Dir.glob("spec/**/*")
|
21
|
+
end
|
data/lib/geocaching.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require "hpricot"
|
2
|
+
|
3
|
+
# This is a Ruby library to access information on geocaching.com. As
|
4
|
+
# Groundspeak does not provide a public API yet, one needs to parse the
|
5
|
+
# website content. That’s what this library does.
|
6
|
+
#
|
7
|
+
# * {Geocaching::Cache} — Represents a cache
|
8
|
+
# * {Geocaching::Log} — Represents a log
|
9
|
+
#
|
10
|
+
# == Usage
|
11
|
+
#
|
12
|
+
# To have access to all information, you will need to provide the
|
13
|
+
# credentials for an account on geocaching.com and log in:
|
14
|
+
#
|
15
|
+
# Geocaching::HTTP.login("username", "password")
|
16
|
+
#
|
17
|
+
# Make sure to log out when you’re done:
|
18
|
+
#
|
19
|
+
# Geocaching::HTTP.logout
|
20
|
+
#
|
21
|
+
# You know have access to all information that are available for the
|
22
|
+
# account you provided.
|
23
|
+
#
|
24
|
+
# cache = Geocaching::Cache.fetch(:code => "GCTEST")
|
25
|
+
# p cache.difficulty #=> 3.5
|
26
|
+
# p cache.latitude #=> 49.17518
|
27
|
+
# p cache.pmonly? #=> false
|
28
|
+
# p cache.logs.size #=> 41
|
29
|
+
#
|
30
|
+
# log = Geocaching::Log.new(:guid => "07208985-f7b2-456a-b0a8-bbc26f28b5a9")
|
31
|
+
# log.fetch
|
32
|
+
# p log.cache #=> #<Geocaching::Cache:...>
|
33
|
+
# p log.username #=> Foobar
|
34
|
+
#
|
35
|
+
module Geocaching
|
36
|
+
# This exception is raised when a method that requires being
|
37
|
+
# logged in is called when not logged in.
|
38
|
+
class LoginError < Exception
|
39
|
+
end
|
40
|
+
|
41
|
+
# This exception is raised when a timeout is hit.
|
42
|
+
class TimeoutError < Exception
|
43
|
+
end
|
44
|
+
|
45
|
+
# This exception is raised when a method is called that requires
|
46
|
+
# the +#fetch+ method to be called first.
|
47
|
+
class NotFetchedError < Exception
|
48
|
+
def initialize
|
49
|
+
super "Need to call the #fetch method first"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# This exception is raised when information could not be
|
54
|
+
# extracted out of the website’s HTML code. For example,
|
55
|
+
# this may happen if Groundspeak changed their website.
|
56
|
+
class ExtractError < Exception
|
57
|
+
end
|
58
|
+
|
59
|
+
# This exception is raised when a HTTP request fails.
|
60
|
+
class HTTPError < Exception
|
61
|
+
end
|
62
|
+
|
63
|
+
autoload :HTTP, "geocaching/http"
|
64
|
+
autoload :Cache, "geocaching/cache"
|
65
|
+
autoload :Log, "geocaching/log"
|
66
|
+
end
|
@@ -0,0 +1,356 @@
|
|
1
|
+
require "time"
|
2
|
+
|
3
|
+
module Geocaching
|
4
|
+
# This class is subclass of Array and is used to store all logs
|
5
|
+
# that belong to a cache. It implements the {#fetch_all} method to
|
6
|
+
# fetch the information of all logs inside the array.
|
7
|
+
class LogsArray < Array
|
8
|
+
# Calls {Geocaching::Log#fetch} for each log inside the array.
|
9
|
+
#
|
10
|
+
# @return [Boolean] Whether all logs could be fetched successfully
|
11
|
+
def fetch_all
|
12
|
+
each { |log| log.fetch }
|
13
|
+
map { |log| log.fetched? }.all?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# This class represents a cache on geocaching.com. Altough some
|
18
|
+
# information are available without being logged in, most information
|
19
|
+
# will only be accessible after a successful login.
|
20
|
+
#
|
21
|
+
# == Example
|
22
|
+
#
|
23
|
+
# cache = Geocaching::Cache.fetch(:code => "GCTEST")
|
24
|
+
#
|
25
|
+
# puts cache.difficulty #=> 3.5
|
26
|
+
# puts cache.latitude #=> 49.741541
|
27
|
+
# puts cache.archived? #=> false
|
28
|
+
#
|
29
|
+
class Cache
|
30
|
+
# Creates a new instance and calls the {#fetch} methods afterwards.
|
31
|
+
# One of +:code+ or +:guid+ must be provided as attributes.
|
32
|
+
#
|
33
|
+
# @param [Hash] attributes A hash of attributes, see {#initialize}
|
34
|
+
# @return [Geocaching::Cache]
|
35
|
+
# @raise [ArgumentError] Tried to set an unknown attribute
|
36
|
+
# @raise [ArgumentError] Neither code nor GUID given
|
37
|
+
def self.fetch(attributes)
|
38
|
+
cache = new(attributes)
|
39
|
+
cache.fetch
|
40
|
+
cache
|
41
|
+
end
|
42
|
+
|
43
|
+
# Creates a new instance. The following attributes may be specified
|
44
|
+
# as parameters:
|
45
|
+
#
|
46
|
+
# * +:code+ — The cache’s GC code
|
47
|
+
# * +:guid+ — The cache’s Globally Unique Identifier
|
48
|
+
#
|
49
|
+
# @param [Hash] attributes A hash of attributes
|
50
|
+
# @raise [ArgumentError] Trying to set an unknown attribute
|
51
|
+
def initialize(attributes = {})
|
52
|
+
@data, @doc, @code, @guid = nil, nil, nil, nil
|
53
|
+
|
54
|
+
attributes.each do |key, value|
|
55
|
+
if [:code, :guid].include?(key)
|
56
|
+
instance_variable_set("@#{key}", value)
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Trying to set unknown attribute `#{key}'"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Fetches cache information from geocaching.com.
|
64
|
+
#
|
65
|
+
# @return [void]
|
66
|
+
# @raise [ArgumentError] Neither code nor GUID are given
|
67
|
+
# @raise [Geocaching::TimeoutError] Timeout hit
|
68
|
+
# @raise [Geocaching::HTTPError] HTTP request failed
|
69
|
+
def fetch
|
70
|
+
raise ArgumentError, "Neither code nor GUID given" unless @code or @guid
|
71
|
+
|
72
|
+
resp, @data = HTTP.get(path)
|
73
|
+
@doc = Hpricot(@data)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Whether information have successfully been fetched
|
77
|
+
# from geocaching.com.
|
78
|
+
#
|
79
|
+
# @return [Boolean] Have information been fetched?
|
80
|
+
def fetched?
|
81
|
+
@data and @doc
|
82
|
+
end
|
83
|
+
|
84
|
+
# The cache’s code (GCXXXXXX).
|
85
|
+
#
|
86
|
+
# @return [String] Code
|
87
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
88
|
+
# @raise [Geocaching::ExtractError] Could not extract code from website
|
89
|
+
def code
|
90
|
+
@code ||= begin
|
91
|
+
raise NotFetchedError unless fetched?
|
92
|
+
elements = @doc.search("#ctl00_uxWaypointName.GCCode")
|
93
|
+
|
94
|
+
if elements.size == 1 and elements.first.inner_html =~ /(GC[A-Z0-9]+)/
|
95
|
+
HTTP.unescape($1)
|
96
|
+
else
|
97
|
+
raise ExtractError, "Could not extract code from website"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# The cache’s Globally Unique Identifier.
|
103
|
+
#
|
104
|
+
# @return [String] GUID
|
105
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
106
|
+
# @raise [Geocaching::ExtractError] Could not extract GUID from website
|
107
|
+
def guid
|
108
|
+
@guid ||= begin
|
109
|
+
raise NotFetchedError unless fetched?
|
110
|
+
|
111
|
+
elements = @doc.search("#ctl00_ContentBody_lnkPrintFriendly")
|
112
|
+
guid = nil
|
113
|
+
|
114
|
+
if elements.size == 1 and href = elements.first.attributes["href"]
|
115
|
+
guid = $1 if href =~ /guid=([0-9a-f-]{36})/
|
116
|
+
end
|
117
|
+
|
118
|
+
guid || begin
|
119
|
+
raise ExtractError, "Could not extract GUID from website"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# The cache’s type ID.
|
125
|
+
#
|
126
|
+
# @return [Fixnum] Type ID
|
127
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
128
|
+
# @raise [Geocaching::ExtractError] Could not extract cache type ID from website
|
129
|
+
def type_id
|
130
|
+
@type_id ||= begin
|
131
|
+
raise NotFetchedError unless fetched?
|
132
|
+
|
133
|
+
if @data =~ /<a.*?title="About Cache Types"><img.*?WptTypes\/(\d+)\.gif".*?<\/a>/
|
134
|
+
$1.to_i
|
135
|
+
else
|
136
|
+
raise ExtractError, "Could not extract cache type ID from website"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# The cache’s name.
|
142
|
+
#
|
143
|
+
# @return [String] Name
|
144
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
145
|
+
# @raise [Geocaching::ExtractError] Could not extract name from website"
|
146
|
+
def name
|
147
|
+
@name ||= begin
|
148
|
+
raise NotFetchedError unless fetched?
|
149
|
+
elements = @doc.search("span#ctl00_ContentBody_CacheName")
|
150
|
+
|
151
|
+
if elements.size == 1
|
152
|
+
HTTP.unescape(elements.first.inner_html)
|
153
|
+
else
|
154
|
+
raise ExtractError, "Could not extract name from website"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# The cache’s difficulty rating.
|
160
|
+
#
|
161
|
+
# @return [Float] Difficulty rating
|
162
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
163
|
+
# @raise [Geocaching::ExtractError] Could not extract difficulty rating from website
|
164
|
+
def difficulty
|
165
|
+
@difficulty ||= begin
|
166
|
+
raise NotFetchedError unless fetched?
|
167
|
+
|
168
|
+
if @data =~ /<strong>\s*?Difficulty:<\/strong>\s*?<img.*?alt="([\d\.]{1,3}) out of 5" \/>/
|
169
|
+
$1.to_f
|
170
|
+
else
|
171
|
+
raise ExtractError, "Could not extract difficulty rating from website"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# The cache’s terrain rating.
|
177
|
+
#
|
178
|
+
# @return [Float] Terrain rating
|
179
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
180
|
+
# @raise [Geocaching::ExtractError] Could not extract terrain rating from website
|
181
|
+
def terrain
|
182
|
+
@terrain ||= begin
|
183
|
+
raise NotFetchedError unless fetched?
|
184
|
+
|
185
|
+
if @data =~ /<strong>\s+?Terrain:<\/strong>\s+?<img.*?alt="([\d\.]{1,3}) out of 5" \/>/
|
186
|
+
$1.to_f
|
187
|
+
else
|
188
|
+
raise ExtractError, "Could not extract terrain rating from website"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# The date the cache has been hidden at.
|
194
|
+
#
|
195
|
+
# @return [Time] Hidden date
|
196
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
197
|
+
# @raise [Geocaching::ExtractError] Could not extract hidden date from website
|
198
|
+
def hidden_at
|
199
|
+
@hidden_at ||= begin
|
200
|
+
raise NotFetchedError unless fetched?
|
201
|
+
|
202
|
+
if @data =~ /<strong>\s+?Hidden\s+?:<\/strong>\s+?(\d{1,2})\/(\d{1,2})\/(\d{4})/
|
203
|
+
Time.parse([$3, $1, $2].join("-"))
|
204
|
+
else
|
205
|
+
raise ExtractError, "Could not extract hidden date from website"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# The cache’s container size.
|
211
|
+
#
|
212
|
+
# @return [Symbol] Cache container size
|
213
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
214
|
+
# @raise [Geocaching::ExtractError] Could not extract cache container size from website
|
215
|
+
def size
|
216
|
+
@size ||= begin
|
217
|
+
raise NotFetchedError unless fetched?
|
218
|
+
size = nil
|
219
|
+
|
220
|
+
if @data =~ /<img src="\/images\/icons\/container\/(.*?)\.gif" alt="Size: .*?" \/>/
|
221
|
+
size = $1.to_sym if %w(micro small regular large other not_chosen).include?($1)
|
222
|
+
end
|
223
|
+
|
224
|
+
size || begin
|
225
|
+
raise ExtractError, "Could not extract cache container size from website"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# The cache’s latitude.
|
231
|
+
#
|
232
|
+
# @return [Float] Latitude
|
233
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
234
|
+
# @raise [Geocaching::ExtractError] Could not extract latitude from website
|
235
|
+
def latitude
|
236
|
+
@latitude ||= begin
|
237
|
+
raise NotFetchedError unless fetched?
|
238
|
+
|
239
|
+
latitude = nil
|
240
|
+
elements = @doc.search("a#ctl00_ContentBody_lnkConversions")
|
241
|
+
|
242
|
+
if elements.size == 1 and href = elements.first.attributes["href"]
|
243
|
+
latitude = $1.to_f if href =~ /lat=(-?[0-9\.]+)/
|
244
|
+
end
|
245
|
+
|
246
|
+
latitude || begin
|
247
|
+
raise ExtractError, "Could not extract latitude from website"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# The cache’s longitude.
|
253
|
+
#
|
254
|
+
# @return [Float] Longitude
|
255
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
256
|
+
# @raise [Geocaching::ExtractError] Could not extract longitude from website
|
257
|
+
def longitude
|
258
|
+
@longitude ||= begin
|
259
|
+
raise NotFetchedError unless fetched?
|
260
|
+
|
261
|
+
longitude = nil
|
262
|
+
elements = @doc.search("a#ctl00_ContentBody_lnkConversions")
|
263
|
+
|
264
|
+
if elements.size == 1 and href = elements.first.attributes["href"]
|
265
|
+
longitude = $1.to_f if href =~ /lon=(-?[0-9\.]+)/
|
266
|
+
end
|
267
|
+
|
268
|
+
longitude || begin
|
269
|
+
raise ExtractError, "Could not extract longitude from website"
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# The cache’s location name (State, Country).
|
275
|
+
#
|
276
|
+
# @return [String] Location name
|
277
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
278
|
+
# @raise [Geocaching::ExtractError] Could not extract location from website
|
279
|
+
def location
|
280
|
+
@location ||= begin
|
281
|
+
raise NotFetchedError unless fetched?
|
282
|
+
|
283
|
+
location = nil
|
284
|
+
elements = @doc.search("span#ctl00_ContentBody_Location")
|
285
|
+
|
286
|
+
if elements.size == 1
|
287
|
+
text = @doc.search("span#ctl00_ContentBody_Location").inner_html
|
288
|
+
location = $1.strip if text =~ /In ([^<]+)/
|
289
|
+
end
|
290
|
+
|
291
|
+
location || begin
|
292
|
+
raise ExtractError, "Could not extract location from website"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Whether the cache has been archived or not.
|
298
|
+
#
|
299
|
+
# @return [Boolean] Has cache been archived?
|
300
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
301
|
+
def archived?
|
302
|
+
@is_archived ||= begin
|
303
|
+
raise NotFetchedError unless fetched?
|
304
|
+
!!(@data =~ /<li>This cache has been archived/)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Whether the cache is only viewable to Premium Member only.
|
309
|
+
#
|
310
|
+
# @return [Boolean] Is cache PM-only?
|
311
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
312
|
+
def pmonly?
|
313
|
+
@is_pmonly ||= begin
|
314
|
+
raise NotFetchedError unless fetched?
|
315
|
+
!!(@data =~ /<p class="Warning">Sorry, the owner of this listing has made it viewable to Premium Members only\./)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns an array of logs for this cache. A log is an instance of
|
320
|
+
# {Geocaching::Log}.
|
321
|
+
#
|
322
|
+
# @return [Geocaching::LogsArray<Geocaching::Log>] Array of logs
|
323
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} first
|
324
|
+
# @raise [Geocaching::ExtractError] Could not extract logs from website
|
325
|
+
def logs
|
326
|
+
@logs ||= begin
|
327
|
+
raise NotFetchedError unless fetched?
|
328
|
+
|
329
|
+
logs = LogsArray.new
|
330
|
+
elements = @doc.search("table.Table.LogsTable > tr > td > strong")
|
331
|
+
|
332
|
+
if elements.size == 0
|
333
|
+
raise ExtractError, "Could not extract logs from website"
|
334
|
+
end
|
335
|
+
|
336
|
+
elements.each do |node|
|
337
|
+
img = node.search("img")
|
338
|
+
a = node.search("a")
|
339
|
+
|
340
|
+
title = img[0]["title"] if img.size == 1 and img[0]["title"]
|
341
|
+
guid = $1 if a.size == 1 and a[0]["href"] and a[0]["href"] =~ /guid=([a-f0-9-]{36})/
|
342
|
+
|
343
|
+
logs << Log.new(:guid => guid, :title => title, :cache => self)
|
344
|
+
end
|
345
|
+
|
346
|
+
logs
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
private
|
351
|
+
|
352
|
+
def path
|
353
|
+
"/seek/cache_details.aspx?log=y&" + (@code ? "wp=#{@code}" : "guid=#{@guid}")
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "net/http"
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module Geocaching
|
6
|
+
class HTTP
|
7
|
+
# The user agent sent with each request.
|
8
|
+
@user_agent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729)"
|
9
|
+
|
10
|
+
# Timeout for sending and receiving HTTP data.
|
11
|
+
@timeout = 8
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :user_agent, :timeout, :username, :password
|
15
|
+
|
16
|
+
# Returns the singleton instance of this class.
|
17
|
+
#
|
18
|
+
# @return [HTTP] Singleton instance of this class
|
19
|
+
def instance
|
20
|
+
@instance ||= new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Alias for:
|
24
|
+
#
|
25
|
+
# Geocaching::HTTP.username = username
|
26
|
+
# Geocaching::HTTP.password = password
|
27
|
+
# Geocaching::HTTP.instance.login
|
28
|
+
def login(username = nil, password = nil)
|
29
|
+
self.username, self.password = username, password if username && password
|
30
|
+
self.instance.login
|
31
|
+
end
|
32
|
+
|
33
|
+
# Alias for +Geocaching::HTTP.instance.logout+.
|
34
|
+
def logout
|
35
|
+
self.instance.logout
|
36
|
+
end
|
37
|
+
|
38
|
+
# Alias for +Geocaching::HTTP.instance.loggedin?+.
|
39
|
+
def loggedin?
|
40
|
+
self.instance.loggedin?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Alias for +Geocaching::HTTP.instance.get+.
|
44
|
+
def get(path)
|
45
|
+
self.instance.get(path)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Alias for +Geocaching::HTTP.instance.post+.
|
49
|
+
def post(path, data = {})
|
50
|
+
self.instance.post(path, data)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Converts HTML entities to the corresponding UTF-8 symbols.
|
54
|
+
#
|
55
|
+
# @return [String] The converted string
|
56
|
+
def unescape(str)
|
57
|
+
CGI.unescapeHTML(str.gsub(/&#(\d{3});/) { [$1.to_i].pack("U") })
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates a new instance.
|
62
|
+
def initialize
|
63
|
+
@loggedin = false
|
64
|
+
@cookie = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# Logs in into geocaching.com. Username and password need to be set
|
68
|
+
# before calling this method.
|
69
|
+
#
|
70
|
+
# HTTP.username = "username"
|
71
|
+
# HTTP.password = "password"
|
72
|
+
#
|
73
|
+
# @return [void]
|
74
|
+
# @raise [ArgumentError] Username or password missing
|
75
|
+
# @raise [Geocaching::LoginError] Already logged in
|
76
|
+
# @raise [Geocaching::TimeoutError] Timeout hit
|
77
|
+
# @raise [Geocaching::HTTPError] HTTP request failed
|
78
|
+
def login
|
79
|
+
raise ArgumentError, "Missing username" unless self.class.username
|
80
|
+
raise ArgumentError, "Missing password" unless self.class.password
|
81
|
+
|
82
|
+
raise LoginError, "Already logged in" if @loggedin
|
83
|
+
|
84
|
+
resp, data = post("/login/default.aspx", {
|
85
|
+
"ctl00$ContentBody$myUsername" => self.class.username,
|
86
|
+
"ctl00$ContentBody$myPassword" => self.class.password,
|
87
|
+
"ctl00$ContentBody$Button1" => "Login",
|
88
|
+
"ctl00$ContentBody$cookie" => "on"
|
89
|
+
})
|
90
|
+
|
91
|
+
@cookie = resp.response["set-cookie"]
|
92
|
+
@loggedin = true
|
93
|
+
end
|
94
|
+
|
95
|
+
# Logs out from geocaching.com.
|
96
|
+
#
|
97
|
+
# @return [void]
|
98
|
+
# @raise [Geocaching::LoginError] Not logged in
|
99
|
+
# @raise [Geocaching::TimeoutError] Timeout hit
|
100
|
+
# @raise [Geocaching::HTTPError] HTTP request failed
|
101
|
+
def logout
|
102
|
+
raise LoginError, "Not logged in" unless @loggedin
|
103
|
+
|
104
|
+
@loggedin = false
|
105
|
+
@cookie = nil
|
106
|
+
|
107
|
+
get("/login/default.aspx?RESET=Y")
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns whether this lib is logged in as a user.
|
111
|
+
#
|
112
|
+
# @return [Boolean] Logged in?
|
113
|
+
def loggedin?
|
114
|
+
@loggedin and @cookie
|
115
|
+
end
|
116
|
+
|
117
|
+
# Sends a GET request to +path+. The authentication cookie is sent
|
118
|
+
# with the request if available.
|
119
|
+
#
|
120
|
+
# @param [String] path
|
121
|
+
# @return [Net::HTTP::Response] Reponse object from +Net::HTTP+
|
122
|
+
# @return [String] Actual content
|
123
|
+
# @raise [Geocaching::TimeoutError] Timeout hit
|
124
|
+
# @raise [Geocaching::HTTPError] HTTP request failed
|
125
|
+
def get(path)
|
126
|
+
resp = data = nil
|
127
|
+
header = default_header
|
128
|
+
header["Cookie"] = @cookie if @cookie
|
129
|
+
|
130
|
+
begin
|
131
|
+
Timeout::timeout(self.class.timeout) do
|
132
|
+
resp, data = http.get(path, header)
|
133
|
+
end
|
134
|
+
rescue Timeout::Error
|
135
|
+
raise TimeoutError, "Timeout hit for GET #{path}"
|
136
|
+
rescue
|
137
|
+
raise HTTPError
|
138
|
+
end
|
139
|
+
|
140
|
+
unless resp.kind_of?(Net::HTTPSuccess) or resp.kind_of?(Net::HTTPRedirection)
|
141
|
+
raise HTTPError
|
142
|
+
end
|
143
|
+
|
144
|
+
[resp, data]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Sends a POST request to +path+ with the data given in the
|
148
|
+
# +params+ hash. The authentication cookie is sent with the
|
149
|
+
# request if available.
|
150
|
+
#
|
151
|
+
# Before sending the POST request, a GET request is sent to obtain the
|
152
|
+
# information like +__VIEWPORT+ that are used on geocaching.com to protect
|
153
|
+
# from Cross Site Request Forgery.
|
154
|
+
#
|
155
|
+
# @return [Net::HTTP::Response] Reponse object from +Net::HTTP+
|
156
|
+
# @return [String] Actual content
|
157
|
+
# @raise [Geocaching::TimeoutError] Timeout hit
|
158
|
+
# @raise [Geocaching::HTTPError] HTTP request failed
|
159
|
+
def post(path, params = {})
|
160
|
+
params = params.merge(metadata(path)).map { |k,v| "#{k}=#{v}" }.join("&")
|
161
|
+
resp = data = nil
|
162
|
+
|
163
|
+
header = default_header
|
164
|
+
header["Cookie"] = @cookie if @cookie
|
165
|
+
|
166
|
+
begin
|
167
|
+
Timeout::timeout(self.class.timeout) do
|
168
|
+
resp, data = http.post(path, params, header)
|
169
|
+
end
|
170
|
+
rescue Timeout::Error
|
171
|
+
raise TimeoutError, "Timeout hit for POST #{path}"
|
172
|
+
rescue
|
173
|
+
raise HTTPError
|
174
|
+
end
|
175
|
+
|
176
|
+
unless resp.kind_of?(Net::HTTPSuccess)
|
177
|
+
raise HTTPError
|
178
|
+
end
|
179
|
+
|
180
|
+
[resp, data]
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
# Sends a GET request to +path+ to obtain form meta data used on
|
186
|
+
# geocaching.com to protect from CSRF.
|
187
|
+
#
|
188
|
+
# @return [Hash] Meta information
|
189
|
+
def metadata(path)
|
190
|
+
resp, data = get(path)
|
191
|
+
meta = {}
|
192
|
+
|
193
|
+
data.scan(/<input type="hidden" name="__([A-Z]+)" id="__[A-Z]+" value="(.*?)" \/>/).each do |match|
|
194
|
+
meta["__#{match[0]}"] = CGI.escape(match[1])
|
195
|
+
end
|
196
|
+
|
197
|
+
meta
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns an hash with the HTTP headers sent with each request.
|
201
|
+
#
|
202
|
+
# @return [Hash] Default HTTP headers
|
203
|
+
def default_header
|
204
|
+
{
|
205
|
+
"User-Agent" => self.class.user_agent
|
206
|
+
}
|
207
|
+
end
|
208
|
+
|
209
|
+
# Returns the instance of {Net::HTTP} or creates a new one for
|
210
|
+
# +www.geocaching.com+.
|
211
|
+
#
|
212
|
+
# @return [Net::HTTP]
|
213
|
+
def http
|
214
|
+
@http ||= begin
|
215
|
+
Net::HTTP.new("www.geocaching.com")
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module Geocaching
|
2
|
+
# This class represents a log on geocaching.com.
|
3
|
+
class Log
|
4
|
+
# Creates a new instance and calls the {#fetch} method afterwards.
|
5
|
+
# +:guid+ must be specified as an attribute.
|
6
|
+
#
|
7
|
+
# @param [Hash] attributes Hash of attributes
|
8
|
+
# @return [Geocaching::Log]
|
9
|
+
# @raise [ArgumentError] Unknown attribute provided
|
10
|
+
# @raise [TypeError] Invalid attribute provided
|
11
|
+
# @raise [Geocaching::TimeoutError] Timeout hit
|
12
|
+
# @raise [Geocaching::HTTPError] HTTP request failed
|
13
|
+
def self.fetch(attributes)
|
14
|
+
log = new(attributes)
|
15
|
+
log.fetch
|
16
|
+
log
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates a new instance. The following attributes may be specified
|
20
|
+
# as parameters:
|
21
|
+
#
|
22
|
+
# * +:guid+ — The log’s Globally Unique Identifier
|
23
|
+
# * +:cache+ — A {Geocaching::Cache} that belongs to this log
|
24
|
+
#
|
25
|
+
# @raise [ArgumentError] Trying to set an unknown attribute
|
26
|
+
# @raise [TypeError] +:code+ is not an instance of {Geocaching::Cache}
|
27
|
+
def initialize(attributes = {})
|
28
|
+
@data, @doc, @guid, @cache = nil, nil, nil, nil
|
29
|
+
|
30
|
+
attributes.each do |key, value|
|
31
|
+
if [:guid, :title, :cache].include?(key)
|
32
|
+
if key == :cache and not value.kind_of?(Geocaching::Cache)
|
33
|
+
raise TypeError, "Attribute `cache' must be an instance of Geocaching::Cache"
|
34
|
+
end
|
35
|
+
|
36
|
+
instance_variable_set("@#{key}", value)
|
37
|
+
else
|
38
|
+
raise ArgumentError, "Trying to set unknown attribute `#{key}'"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Fetches log information from geocaching.com.
|
44
|
+
#
|
45
|
+
# @return [void]
|
46
|
+
# @raise [ArgumentError] GUID is not given
|
47
|
+
# @raise [Geocaching::LoginError] Not logged in
|
48
|
+
# @raise [Geocaching::TimeoutError] Timeout hit
|
49
|
+
# @raise [Geocaching::HTTPError] HTTP request failed
|
50
|
+
def fetch
|
51
|
+
raise ArgumentError, "No GUID given" unless @guid
|
52
|
+
raise LoginError, "Need to be logged in to fetch log information" unless HTTP.loggedin?
|
53
|
+
|
54
|
+
resp, @data = HTTP.get(path)
|
55
|
+
@doc = Hpricot(@data)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Whether information have successfully been fetched
|
59
|
+
# from geocaching.com.
|
60
|
+
#
|
61
|
+
# @return [Boolean] Have information beed fetched?
|
62
|
+
def fetched?
|
63
|
+
@data and @doc
|
64
|
+
end
|
65
|
+
|
66
|
+
# The cache that belongs to this log.
|
67
|
+
#
|
68
|
+
# @return [Geocaching::Cache] Cache
|
69
|
+
def cache
|
70
|
+
@cache ||= begin
|
71
|
+
if guid = cache_guid
|
72
|
+
Cache.new(:guid => guid)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def title
|
78
|
+
@title
|
79
|
+
end
|
80
|
+
|
81
|
+
# The name of the user that has posted this log.
|
82
|
+
#
|
83
|
+
# @return [String] Username
|
84
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} before
|
85
|
+
# @raise [Geocaching::ExtractError] Could not extract username from website
|
86
|
+
def username
|
87
|
+
@username ||= begin
|
88
|
+
raise NotFetchedError unless fetched?
|
89
|
+
|
90
|
+
elements = @doc.search("#ctl00_ContentBody_LogBookPanel1_lbLogText > a")
|
91
|
+
|
92
|
+
if elements.size > 0
|
93
|
+
HTTP.unescape(elements.first.inner_html)
|
94
|
+
else
|
95
|
+
raise ExtractError, "Could not extract username from website"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# The log’s raw message with all format codes.
|
101
|
+
#
|
102
|
+
# @return [String] Log message
|
103
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} before
|
104
|
+
# @raise [Geocaching::ExtractError] Could not extract message from website
|
105
|
+
def message
|
106
|
+
@message ||= begin
|
107
|
+
raise NotFetchedError unless fetched?
|
108
|
+
|
109
|
+
if meta[:description]
|
110
|
+
meta[:description].gsub(/\r\n/, "\n")
|
111
|
+
else
|
112
|
+
raise ExtractError, "Could not extract message from website"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# The short coord.info URL for this log.
|
118
|
+
#
|
119
|
+
# @return [String] coord.info URL
|
120
|
+
# @raise [Geocaching::NotFetchedError] Need to call {#fetch} before
|
121
|
+
# @raise [Geocaching::ExtractError] Could not extract short URL from website
|
122
|
+
def short_url
|
123
|
+
@short_url ||= begin
|
124
|
+
raise NotFetchedError unless fetched?
|
125
|
+
|
126
|
+
meta[:url] || begin
|
127
|
+
raise ExtractError, "Could not extract short URL from website"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def cache_guid
|
135
|
+
if @cache.kind_of?(Geocaching::Cache) and @cache.guid
|
136
|
+
@cache.guid
|
137
|
+
else
|
138
|
+
raise NotFetchedError unless fetched?
|
139
|
+
|
140
|
+
elements = @doc.search("#ctl00_ContentBody_LogBookPanel1_lbLogText > a")
|
141
|
+
|
142
|
+
if elements.size == 2 and elements[1]["href"]
|
143
|
+
elements[1]["href"] =~ /guid=([a-f0-9-]{36})/
|
144
|
+
$1
|
145
|
+
else
|
146
|
+
raise ExtractError, "Could not extract cache GUID from website"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def meta
|
152
|
+
@meta ||= begin
|
153
|
+
elements = @doc.search("meta").select { |e| e.attributes["name"] =~ /^og:/ }.flatten
|
154
|
+
info = {}
|
155
|
+
|
156
|
+
elements.each do |element|
|
157
|
+
info[element["name"].gsub(/^og:/, "").to_sym] = element["content"]
|
158
|
+
end
|
159
|
+
|
160
|
+
info
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def path
|
165
|
+
"/seek/log.aspx?LUID=#{@guid}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/spec/cache_spec.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
2
|
+
|
3
|
+
require "geocaching"
|
4
|
+
require "helper"
|
5
|
+
|
6
|
+
share_as :GCF00 do
|
7
|
+
it "should return the correct GC code" do
|
8
|
+
@cache.code.should == "GCF00"
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return the correct GUID" do
|
12
|
+
@cache.guid.should == "66274935-40d5-43d8-8cc3-c819e38f9dcc"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return correct cache type ID" do
|
16
|
+
@cache.type_id.should == 2
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return the correct name" do
|
20
|
+
@cache.name.should == "Bridge Over Troubled Waters"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return the correct latitude" do
|
24
|
+
@cache.latitude.should == 32.6684
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return the correct longitude" do
|
28
|
+
@cache.longitude.should == -97.436783
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return the correct difficulty rating" do
|
32
|
+
@cache.difficulty.should == 2.5
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should return the correct terrain rating" do
|
36
|
+
@cache.terrain.should == 1.5
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should return the correct hidden at date" do
|
40
|
+
@cache.hidden_at.should == Time.parse("2001-07-05")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should return the correct location" do
|
44
|
+
@cache.location.should == "Texas, United States"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should say cache is not archived" do
|
48
|
+
@cache.archived?.should == false
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should say cache is not PM-only" do
|
52
|
+
@cache.pmonly?.should == false
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should return a plausible number of total logs" do
|
56
|
+
@cache.logs.size.should >= 230
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "Geocaching::Cache for GCF00" do
|
61
|
+
before :all do
|
62
|
+
@cache = Geocaching::Cache.fetch(:code => "GCF00")
|
63
|
+
end
|
64
|
+
|
65
|
+
include GCF00
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "Geocaching::Cache for 66274935-40d5-43d8-8cc3-c819e38f9dcc" do
|
69
|
+
before :all do
|
70
|
+
@cache = Geocaching::Cache.fetch(:guid => "66274935-40d5-43d8-8cc3-c819e38f9dcc")
|
71
|
+
end
|
72
|
+
|
73
|
+
include GCF00
|
74
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
unless ENV["GC_USERNAME"] and ENV["GC_PASSWORD"]
|
2
|
+
$stderr.puts "You need to provide your geocaching.com account credentials"
|
3
|
+
$stderr.puts "by setting the environment variables GC_USERNAME and GC_PASSWORD."
|
4
|
+
exit 1
|
5
|
+
end
|
6
|
+
|
7
|
+
begin
|
8
|
+
Geocaching::HTTP.login(ENV["GC_USERNAME"], ENV["GC_PASSWORD"])
|
9
|
+
rescue Geocaching::Error => e
|
10
|
+
$stderr.puts "Failed to log in: #{e.message}"
|
11
|
+
exit 1
|
12
|
+
end
|
data/spec/log_spec.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
2
|
+
|
3
|
+
require "geocaching"
|
4
|
+
require "helper"
|
5
|
+
|
6
|
+
describe "Geocaching::Log for a3237dec-6931-4221-8f00-5d62923b411a" do
|
7
|
+
before :all do
|
8
|
+
@log = Geocaching::Log.fetch(:guid => "a3237dec-6931-4221-8f00-5d62923b411a")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return the correct username" do
|
12
|
+
@log.username.should == "CampinCrazy"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return the correct cache GUID" do
|
16
|
+
@log.cache.guid.should == "66274935-40d5-43d8-8cc3-c819e38f9dcc"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return the correct message" do
|
20
|
+
should_message = File.read(File.join(File.dirname(__FILE__), "log_message.txt"))
|
21
|
+
@log.message.should == should_message.gsub(/\r\n/, "\n")
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: geocaching
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- nano
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-07-25 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: |
|
22
|
+
A Ruby library that allows to access information on geocaching.com
|
23
|
+
in an object-oriented manner.
|
24
|
+
|
25
|
+
email: nano@fooo.org
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- README.markdown
|
34
|
+
- Rakefile
|
35
|
+
- geocaching.gemspec
|
36
|
+
- lib/geocaching/cache.rb
|
37
|
+
- lib/geocaching/http.rb
|
38
|
+
- lib/geocaching/log.rb
|
39
|
+
- lib/geocaching.rb
|
40
|
+
- spec/cache_spec.rb
|
41
|
+
- spec/helper.rb
|
42
|
+
- spec/log_message.txt
|
43
|
+
- spec/log_spec.rb
|
44
|
+
has_rdoc: yard
|
45
|
+
homepage: http://nano.github.com/geocaching
|
46
|
+
licenses: []
|
47
|
+
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.7
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Library for accessing information on geocaching.com
|
76
|
+
test_files: []
|
77
|
+
|