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.
@@ -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
@@ -0,0 +1,9 @@
1
+ task :default => :test
2
+
3
+ task :test do
4
+ sh "spec -c -f specdoc spec"
5
+ end
6
+
7
+ task :console do
8
+ sh "irb -I lib -r geocaching"
9
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ We almost made this really tough to get to with the surrounding terrain, then we spotted a better way. Sneaky grab as pearle and I cached our way across southern FW.
2
+ [br]
3
+ Thanks for the fun!
4
+ [br]
5
+ [b][green]CC [:P][/b][/green]
@@ -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
+