strava 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +6 -0
- data/.yardopts +1 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +302 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/strava.rb +79 -0
- data/lib/strava/activity.rb +224 -0
- data/lib/strava/adapters/httparty_adapter.rb +9 -0
- data/lib/strava/athlete.rb +378 -0
- data/lib/strava/base.rb +86 -0
- data/lib/strava/client.rb +65 -0
- data/lib/strava/club.rb +164 -0
- data/lib/strava/club_announcement.rb +28 -0
- data/lib/strava/comment.rb +36 -0
- data/lib/strava/error.rb +15 -0
- data/lib/strava/gear.rb +41 -0
- data/lib/strava/group_event.rb +62 -0
- data/lib/strava/lap.rb +39 -0
- data/lib/strava/leaderboard.rb +57 -0
- data/lib/strava/leaderboard_entry.rb +48 -0
- data/lib/strava/photo.rb +50 -0
- data/lib/strava/route.rb +51 -0
- data/lib/strava/running_race.rb +50 -0
- data/lib/strava/segment.rb +100 -0
- data/lib/strava/segment_effort.rb +45 -0
- data/lib/strava/stream.rb +33 -0
- data/lib/strava/stream_set.rb +62 -0
- data/lib/strava/usage.rb +38 -0
- data/lib/strava/version.rb +4 -0
- data/strava.gemspec +29 -0
- metadata +135 -0
data/lib/strava/base.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Strava
|
2
|
+
# Base class for Strava objects.
|
3
|
+
# Handles setting up the object, mainly data and a client.
|
4
|
+
#
|
5
|
+
# @abstract
|
6
|
+
class Base
|
7
|
+
attr_reader :response, :client, :id
|
8
|
+
|
9
|
+
def initialize(data, client: nil, token: nil, **opts)
|
10
|
+
raise 'missing client or access token' unless (client || token)
|
11
|
+
@client = client || Client.new(token)
|
12
|
+
if data.is_a?(Hash)
|
13
|
+
@id = data['id']
|
14
|
+
set_ivars
|
15
|
+
update(data, **opts)
|
16
|
+
else
|
17
|
+
@id = data
|
18
|
+
set_ivars
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse incoming data.
|
23
|
+
# Should be defined by subclasses.
|
24
|
+
#
|
25
|
+
# @abstract
|
26
|
+
def update(data, **opts)
|
27
|
+
@response = data
|
28
|
+
@resource_state = data['resource_state']
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set up instance variables upon instantiation.
|
33
|
+
# Should be defined by subclasses.
|
34
|
+
# May not always be necessary.
|
35
|
+
#
|
36
|
+
# @abstract
|
37
|
+
# @return [void]
|
38
|
+
private def set_ivars
|
39
|
+
# this should be defined by subclasses
|
40
|
+
end
|
41
|
+
|
42
|
+
private def parse_data(existing, data, klass: nil, **opts)
|
43
|
+
existing ||= {}
|
44
|
+
case data
|
45
|
+
when [], {}
|
46
|
+
[]
|
47
|
+
when Array
|
48
|
+
data.map do |hash|
|
49
|
+
current = existing[hash['id']]
|
50
|
+
if current
|
51
|
+
current.send(:update, hash, **opts)
|
52
|
+
else
|
53
|
+
current = klass.new(hash, **opts)
|
54
|
+
existing[current.id] = current
|
55
|
+
end
|
56
|
+
existing[current.id]
|
57
|
+
end
|
58
|
+
when Hash
|
59
|
+
existing[data['id']] = klass.new(data, **opts)
|
60
|
+
else
|
61
|
+
# raise
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def resource_state
|
66
|
+
self.class.resource_states[@resource_state]
|
67
|
+
end
|
68
|
+
|
69
|
+
def summary?
|
70
|
+
@resource_state == 2
|
71
|
+
end
|
72
|
+
|
73
|
+
def detailed?
|
74
|
+
@resource_state == 3
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.resource_states
|
78
|
+
@resource_states ||= {
|
79
|
+
1 => 'meta',
|
80
|
+
2 => 'summary',
|
81
|
+
3 => 'detailed',
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
module Strava
|
3
|
+
class Client
|
4
|
+
attr_reader :token
|
5
|
+
# @return [Usage] Information on API quota usage
|
6
|
+
attr_reader :usage
|
7
|
+
BASE_URL = 'https://www.strava.com/api/v3/' # can be overridden for individual requests
|
8
|
+
|
9
|
+
def initialize(token)
|
10
|
+
@token = token
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(path, **params)
|
14
|
+
make_request(:get, path, **params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(path, **params)
|
18
|
+
make_request(:post, path, **params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def put(path, **params)
|
22
|
+
make_request(:put, path, **params)
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(path, **params)
|
26
|
+
make_request(:delete, path, **params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def make_request(verb, path, **params)
|
30
|
+
puts (params[:host] || BASE_URL) + path
|
31
|
+
handle_params(params)
|
32
|
+
res = HTTParty.send(verb, (params.delete(:host) || BASE_URL) + path, query: params)
|
33
|
+
check_for_error(res)
|
34
|
+
res
|
35
|
+
end
|
36
|
+
|
37
|
+
def handle_params(params)
|
38
|
+
if @token
|
39
|
+
params.merge!(access_token: @token)
|
40
|
+
else
|
41
|
+
params.merge!(client_id: Strava.client_id, client_secret: Strava.secret)
|
42
|
+
end
|
43
|
+
params.reverse_each { |k, v| params.delete(k) if v.nil? }
|
44
|
+
end
|
45
|
+
|
46
|
+
def check_for_error(response)
|
47
|
+
@usage = Usage.new(response.headers['X-Ratelimit-Limit'], response.headers['X-Ratelimit-Usage'])
|
48
|
+
case response.code
|
49
|
+
when 401, 403
|
50
|
+
raise Strava::AccessError.new(response.to_h)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
## non athlete calls
|
56
|
+
def list_races(year = Time.now.year)
|
57
|
+
RunningRace.list_races(self, year)
|
58
|
+
end
|
59
|
+
|
60
|
+
def segment_explorer(bounds = '37.821362,-122.505373,37.842038,-122.465977')
|
61
|
+
Segment.explorer(self, bounds)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/strava/club.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
module Strava
|
2
|
+
# Clubs represent groups of athletes on Strava. They can be public or private.
|
3
|
+
# Clubs have both summary and detailed representations.
|
4
|
+
#
|
5
|
+
# @see https://strava.github.io/api/v3/clubs/ Strava Docs - Clubs
|
6
|
+
class Club < Base
|
7
|
+
|
8
|
+
# Set up instance variables upon instantiation.
|
9
|
+
#
|
10
|
+
# @abstract
|
11
|
+
# @return [void]
|
12
|
+
private def set_ivars
|
13
|
+
@activities = {}
|
14
|
+
@group_events = {}
|
15
|
+
@announcements = []
|
16
|
+
@members = {}
|
17
|
+
@admins = []
|
18
|
+
@segment_efforts = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Update an existing club.
|
22
|
+
# Used by other methods in the gem.
|
23
|
+
# Should not be used directly.
|
24
|
+
#
|
25
|
+
# @param data [Hash] data to update the club with
|
26
|
+
# @return [self]
|
27
|
+
def update(data, **opts)
|
28
|
+
@response = data
|
29
|
+
@id = data["id"]
|
30
|
+
@resource_state = data['resource_state']
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def activities(per_page: nil, page: nil, before: nil)
|
36
|
+
if page || per_page || before
|
37
|
+
get_activities(per_page: per_page, page: page, before: before)
|
38
|
+
else
|
39
|
+
get_activities if @activities.empty?
|
40
|
+
@activities.values
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def group_events(per_page: nil, page: nil, before: nil)
|
45
|
+
if page || per_page || before
|
46
|
+
get_group_events(per_page: per_page, page: page, before: before)
|
47
|
+
else
|
48
|
+
get_group_events if @group_events.empty?
|
49
|
+
@group_events.values
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def announcements
|
54
|
+
get_announcements if @announcements.empty?
|
55
|
+
@announcements
|
56
|
+
end
|
57
|
+
|
58
|
+
def members(per_page: nil, page: nil)
|
59
|
+
if page || per_page
|
60
|
+
get_members(per_page: per_page, page: page)
|
61
|
+
else
|
62
|
+
get_members if @members.empty? || !@members_fetched
|
63
|
+
@members_fetched = true
|
64
|
+
@members.values
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def admins(per_page: nil, page: nil)
|
69
|
+
if page || per_page
|
70
|
+
get_admins(per_page: per_page, page: page)
|
71
|
+
else
|
72
|
+
get_admins if @admins.empty?
|
73
|
+
@admins
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# {"success"=>true, "active"=>false}
|
78
|
+
def join
|
79
|
+
res = client.post(path_join).to_h
|
80
|
+
end
|
81
|
+
|
82
|
+
# {"success"=>true, "active"=>true, "membership"=>"member"}
|
83
|
+
def leave
|
84
|
+
res = client.post(path_leave).to_h
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_details
|
88
|
+
return self if detailed?
|
89
|
+
res = client.get(path_base).to_h
|
90
|
+
update(res)
|
91
|
+
end
|
92
|
+
|
93
|
+
private def get_activities(per_page: nil, page: nil, before: nil)
|
94
|
+
res = client.get(path_activities, per_page: per_page, page: page, before: before).to_a
|
95
|
+
parse_data(@activities, res, klass: Activity, client: @client)
|
96
|
+
end
|
97
|
+
|
98
|
+
private def get_group_events(per_page: nil, page: nil, before: nil)
|
99
|
+
res = client.get(path_group_events, per_page: per_page, page: page, before: before).to_a
|
100
|
+
parse_data(@group_events, res, klass: Activity, client: @client)
|
101
|
+
end
|
102
|
+
|
103
|
+
private def get_announcements
|
104
|
+
res = client.get(path_announcements).to_a
|
105
|
+
@announcements = parse_data({}, res, klass: ClubAnnouncement, client: @client)
|
106
|
+
end
|
107
|
+
|
108
|
+
private def get_members
|
109
|
+
res = client.get(path_members).to_a
|
110
|
+
parse_data(@members, res, klass: Athlete, client: @client)
|
111
|
+
end
|
112
|
+
|
113
|
+
private def get_admins
|
114
|
+
res = client.get(path_admins).to_a
|
115
|
+
@admins = parse_data(@members, res, klass: Athlete, client: @client)
|
116
|
+
end
|
117
|
+
|
118
|
+
private def path_base
|
119
|
+
"clubs/#{id}"
|
120
|
+
end
|
121
|
+
|
122
|
+
private def path_activities
|
123
|
+
"#{path_base}/activities"
|
124
|
+
end
|
125
|
+
|
126
|
+
private def path_group_events
|
127
|
+
"#{path_base}/group_events"
|
128
|
+
end
|
129
|
+
|
130
|
+
private def path_announcements
|
131
|
+
"#{path_base}/announcements"
|
132
|
+
end
|
133
|
+
|
134
|
+
private def path_members
|
135
|
+
"#{path_base}/members"
|
136
|
+
end
|
137
|
+
|
138
|
+
private def path_admins
|
139
|
+
"#{path_base}/admins"
|
140
|
+
end
|
141
|
+
|
142
|
+
private def path_join
|
143
|
+
"#{path_base}/join"
|
144
|
+
end
|
145
|
+
|
146
|
+
private def path_leave
|
147
|
+
"#{path_base}/leave"
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
__END__
|
154
|
+
|
155
|
+
ca = Strava::Athlete.current_athlete;
|
156
|
+
club = ca.clubs.last
|
157
|
+
club.admins
|
158
|
+
club.members
|
159
|
+
club.get_details
|
160
|
+
club.activities
|
161
|
+
club.group_events
|
162
|
+
club.announcements
|
163
|
+
club.leave
|
164
|
+
club.join
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Strava
|
2
|
+
# Class to represent Strava Club Announcement
|
3
|
+
# https://strava.github.io/api/v3/activities/
|
4
|
+
class ClubAnnouncement < Base
|
5
|
+
|
6
|
+
def update(data, **opts)
|
7
|
+
@response = data
|
8
|
+
@id = data['id']
|
9
|
+
@resource_state = data['resource_state']
|
10
|
+
|
11
|
+
@message = data['message']
|
12
|
+
@created_at = data['created_at']
|
13
|
+
@club_id = data['club_id']
|
14
|
+
@athlete = Athlete.new(data['athlete'], client: @client)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
__END__
|
21
|
+
|
22
|
+
ca = Strava::Athlete.current_athlete;
|
23
|
+
ca.activities;
|
24
|
+
ca.activities(page: 2);
|
25
|
+
ca.activities(page: 3);
|
26
|
+
ca.activities(page: 4);
|
27
|
+
act = ca.activities.detect{|act| act.response['comment_count'] > 0 && act.response['kudos_count'] > 0 }
|
28
|
+
act.comments
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Strava
|
2
|
+
# Class to represent Strava Activity
|
3
|
+
# https://strava.github.io/api/v3/activities/
|
4
|
+
class Comment < Base
|
5
|
+
|
6
|
+
attr_reader :activity_id
|
7
|
+
|
8
|
+
def update(data, **opts)
|
9
|
+
@response = data
|
10
|
+
@id = data['id']
|
11
|
+
@resource_state = data['resource_state']
|
12
|
+
|
13
|
+
@text = data['text']
|
14
|
+
@activity_id = data['activity_id']
|
15
|
+
@athlete = Athlete.new(data['athlete'], client: @client)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete
|
19
|
+
res = client.delete(path_base).to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
def path_base
|
23
|
+
"activities/#{activity_id}/comments/#{id}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
__END__
|
29
|
+
|
30
|
+
ca = Strava::Athlete.current_athlete;
|
31
|
+
ca.activities;
|
32
|
+
ca.activities(page: 2);
|
33
|
+
ca.activities(page: 3);
|
34
|
+
ca.activities(page: 4);
|
35
|
+
act = ca.activities.detect{|act| act.response['comment_count'] > 0 && act.response['kudos_count'] > 0 }
|
36
|
+
act.comments
|
data/lib/strava/error.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
{"message"=>"Authorization Error", "errors"=>[{"resource"=>"AccessToken", "field"=>"write_permission", "code"=>"missing"}]}
|
2
|
+
|
3
|
+
module Strava
|
4
|
+
class Error < StandardError
|
5
|
+
attr_accessor :response, :strava_errors
|
6
|
+
end
|
7
|
+
|
8
|
+
class AccessError < Error
|
9
|
+
def initialize(response)
|
10
|
+
message = response['message']
|
11
|
+
strava_errors = response['errors']
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/strava/gear.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Strava
|
2
|
+
# Gear represents both shoes and bikes.
|
3
|
+
# These are returned as part of the athlete summary.
|
4
|
+
#
|
5
|
+
# @see https://strava.github.io/api/v3/gear/ Strava Gear API Docs
|
6
|
+
class Gear < Base
|
7
|
+
|
8
|
+
# Updates gear with passed data attributes.
|
9
|
+
#
|
10
|
+
# @param data [Hash] data hash containing gear data
|
11
|
+
# @return [self]
|
12
|
+
def update(data, **opts)
|
13
|
+
@response = data
|
14
|
+
@id = data['id']
|
15
|
+
@resource_state = data['resource_state']
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
# Retrieve full details for Gear object.
|
20
|
+
# Sets all data attributes on self.
|
21
|
+
#
|
22
|
+
# @return [Hash] raw API response
|
23
|
+
def get_details
|
24
|
+
return self if detailed?
|
25
|
+
res = client.get(path_base).to_h
|
26
|
+
update(res)
|
27
|
+
res
|
28
|
+
end
|
29
|
+
|
30
|
+
# URL path for Gear object.
|
31
|
+
#
|
32
|
+
# @return [String] URL path
|
33
|
+
private def path_base
|
34
|
+
"gear/#{id}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
__END__
|
40
|
+
|
41
|
+
ca = Strava::Athlete.current_athlete;
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Strava
|
2
|
+
# Group events for Strava Clubs
|
3
|
+
#
|
4
|
+
# @see http://strava.github.io/api/v3/club_group_events/ Strava Docs - Group Events
|
5
|
+
class GroupEvent < Base
|
6
|
+
|
7
|
+
def update(data, **opts)
|
8
|
+
@response = data
|
9
|
+
@id = data["id"]
|
10
|
+
@resource_state = data['resource_state']
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_details
|
14
|
+
return self if detailed?
|
15
|
+
res = client.get(path_base).to_h
|
16
|
+
update(res)
|
17
|
+
res
|
18
|
+
end
|
19
|
+
|
20
|
+
def athletes(per_page: nil, page: nil)
|
21
|
+
if page || per_page
|
22
|
+
get_athletes(per_page: per_page, page: page)
|
23
|
+
else
|
24
|
+
get_athletes if @athletes.empty?
|
25
|
+
@athletes.values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def delete
|
30
|
+
res = client.delete(path_base).to_h
|
31
|
+
end
|
32
|
+
|
33
|
+
# {"success"=>true, "active"=>false}
|
34
|
+
def join
|
35
|
+
res = client.post(path_rsvp).to_h
|
36
|
+
end
|
37
|
+
|
38
|
+
# {"success"=>true, "active"=>true, "membership"=>"member"}
|
39
|
+
def leave
|
40
|
+
res = client.delete(path_rsvp).to_h
|
41
|
+
end
|
42
|
+
|
43
|
+
private def path_base
|
44
|
+
"group_events/#{id}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private def path_rsvp
|
48
|
+
"#{path_base}/rsvps"
|
49
|
+
end
|
50
|
+
|
51
|
+
private def path_athletes
|
52
|
+
"#{path_base}/athletes"
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
__END__
|
59
|
+
|
60
|
+
ca = Strava::Athlete.current_athlete;
|
61
|
+
miz = ca.clubs.last;
|
62
|
+
miz.group_events
|