diff --git a/CHANGELOG.md b/CHANGELOG.md index 665a593..8c5a2dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ### 3.0.0 (Next) -* [#93](https://github.com/dblock/strava-ruby-client/pull/93): Updates GitHub Actions workflows - [@simonneutert](https://github.com/simonneutert). +* [#94](https://github.com/dblock/strava-ruby-client/pull/94): Adds video fields to `Strava::Models::Photo` - [@dblock](https://github.com/dblock). * [#92](https://github.com/dblock/strava-ruby-client/pull/92): Fixes `Hashie::Trash` serialization warning for `object_id` of `Strava::Webhooks::Models::Event` - [@simonneutert](https://github.com/simonneutert). +* [#93](https://github.com/dblock/strava-ruby-client/pull/93): Updates GitHub Actions workflows - [@simonneutert](https://github.com/simonneutert). * Your contribution here. ### 2.3.0 (2025/10/16) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8abd81f..52d2707 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,25 @@ bundle exec rake ## Contribute Code +### Obtain a Strava Token + +The token from the Strava website does not have enough permissions to retrieve your own activities. +Use the [strava-oauth-token tool](#strava-oauth-token) to obtain a short lived with more access scopes. + +Obtain `STRAVA_CLIENT_ID` and `STRAVA_CLIENT_SECRET` from [My API Application](https://www.strava.com/settings/api). + +```bash +export STRAVA_CLIENT_ID=... +export STRAVA_CLIENT_SECRET=... +bundle exec ruby bin/strava-oauth-token +``` + +This will open a browser window. Complete the OAuth workflow and note `access_token`. + +``` +export STRAVA_ACCESS_TOKEN=... +``` + ### Create a Topic Branch Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. diff --git a/UPGRADING.md b/UPGRADING.md index e214e42..ed7ab33 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,14 @@ ### Upgrading to >= 3.0.0 +#### Removed `id` from `Strava::Models::Photo` + +The Strava Photos API returns `unique_id` and does not return `id`. The latter has been removed. + +See [#94](https://github.com/dblock/strava-ruby-client/pull/94) for details. + +#### Renamed `object_id` to `id` in `Strava::Webhooks::Models::Event` + The `Strava::Webhooks::Models::Event` model has been refactored to map the `object_id` field to `id` for consistency and to resolve `Hashie::Trash` serialization warnings. **Breaking Change**: If you're using webhooks and accessing the `object_id` property, you must now use `id` instead. diff --git a/lib/strava/models/photo.rb b/lib/strava/models/photo.rb index 7e4c76c..21bb93b 100644 --- a/lib/strava/models/photo.rb +++ b/lib/strava/models/photo.rb @@ -3,20 +3,27 @@ module Strava module Models class Photo < Strava::Models::Response - property 'id' property 'unique_id' - property 'urls' - property 'source' property 'athlete_id' property 'activity_id' property 'activity_name' + property 'post_id' property 'resource_state' property 'caption' + property 'type' + property 'source' + property 'status' + property 'uploaded_at', transform_with: ->(v) { Time.parse(v) } property 'created_at', transform_with: ->(v) { Time.parse(v) } property 'created_at_local', transform_with: ->(v) { Time.parse(v) } - property 'uploaded_at', transform_with: ->(v) { Time.parse(v) } + property 'urls' + property 'placeholder_image' property 'sizes' property 'default_photo' + property 'cursor' + property 'duration' + property 'video_url' + property 'location' end end end diff --git a/spec/fixtures/strava/client/activity_photos.yml b/spec/fixtures/strava/client/activity_photos.yml index 918251f..f1860a7 100644 --- a/spec/fixtures/strava/client/activity_photos.yml +++ b/spec/fixtures/strava/client/activity_photos.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://www.strava.com/api/v3/activities/7287327028/photos?size=5000 + uri: https://www.strava.com/api/v3/activities/16181809559/photos?size=5000 body: encoding: US-ASCII string: '' @@ -12,7 +12,7 @@ http_interactions: Accept: - application/json; charset=utf-8 User-Agent: - - Strava Ruby Client/2.1.1 + - Strava Ruby Client/3.0.0 Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 response: @@ -27,128 +27,55 @@ http_interactions: Connection: - keep-alive Date: - - Mon, 16 Sep 2024 09:55:25 GMT + - Sat, 18 Oct 2025 20:08:37 GMT X-Envoy-Upstream-Service-Time: - - '66' + - '72' Server: - istio-envoy - Via: - - 1.1 linkerd, 1.1 29051585a13addd312c8ac9d527433c6.cloudfront.net (CloudFront) - Etag: - - W/"cbeec23872d5a48d286f8fba6d505930" - Vary: - - Accept, Origin Status: - 200 OK - X-Request-Id: - - 884a63b9-1047-4a4c-bf2e-05db3a2e109b - Cache-Control: - - max-age=0, private, must-revalidate - Referrer-Policy: - - strict-origin-when-cross-origin - X-Frame-Options: - - DENY - X-Xss-Protection: - - 1; mode=block - X-Ratelimit-Limit: - - '600,6000' X-Ratelimit-Usage: - - '11,11' - X-Download-Options: - - noopen - X-Readratelimit-Limit: - - '300,3000' - X-Readratelimit-Usage: - - '11,11' - X-Content-Type-Options: - - nosniff - X-Permitted-Cross-Domain-Policies: - - none - X-Cache: - - Miss from cloudfront - X-Amz-Cf-Pop: - - FRA2-C1 - X-Amz-Cf-Id: - - xgv_HWH_sACuA0be_EzGGbd1Q8gXbPuOaVpMLouNzD58d5EpeIUWRA== - body: - encoding: UTF-8 - string: "[{\"unique_id\":\"f5ebd6e7-8c87-4478-86ce-ce5cf31cf519\",\"athlete_id\":24776507,\"activity_id\":7287327028,\"activity_name\":\"Die - Rückkehr der Cornichons! \U0001F952\",\"post_id\":null,\"resource_state\":2,\"caption\":\"\",\"type\":1,\"source\":1,\"status\":3,\"uploaded_at\":\"2022-06-10T20:41:21Z\",\"created_at\":\"2022-06-10T20:41:03Z\",\"created_at_local\":\"2022-06-10T22:41:03Z\",\"urls\":{\"5000\":\"https://dgtzuqphqg23d.cloudfront.net/3wt2DyGHKHX6gJSXzpAcVdEI1QE2luP9xoDLD0CX2w4-2048x1536.jpg\"},\"placeholder_image\":null,\"sizes\":{\"5000\":[2048,1536]},\"default_photo\":true,\"cursor\":null},{\"unique_id\":\"67eba19c-c8d1-4cc7-b910-b1b4755eccd1\",\"athlete_id\":24776507,\"activity_id\":7287327028,\"activity_name\":\"Die - Rückkehr der Cornichons! \U0001F952\",\"post_id\":null,\"resource_state\":2,\"caption\":\"\",\"type\":1,\"source\":1,\"status\":3,\"uploaded_at\":\"2022-06-10T20:41:20Z\",\"created_at\":\"2022-06-10T20:41:02Z\",\"created_at_local\":\"2022-06-10T22:41:02Z\",\"urls\":{\"5000\":\"https://dgtzuqphqg23d.cloudfront.net/WS55P6XH57QMUGn9-b7cpBQRc9XeDDsLXREDaCKrjig-2048x1536.jpg\"},\"placeholder_image\":null,\"sizes\":{\"5000\":[2048,1536]},\"default_photo\":false,\"cursor\":null}]" - recorded_at: Mon, 16 Sep 2024 09:55:25 GMT -- request: - method: get - uri: https://www.strava.com/api/v3/activities/3958491750/photos?size=5000 - body: - encoding: US-ASCII - string: '' - headers: - Authorization: - - Bearer access-token - Accept: - - application/json; charset=utf-8 - User-Agent: - - Strava Ruby Client/2.1.1 - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - response: - status: - code: 200 - message: OK - headers: - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - keep-alive - Date: - - Mon, 16 Sep 2024 09:55:25 GMT - X-Envoy-Upstream-Service-Time: - - '64' - Server: - - istio-envoy - Via: - - 1.1 linkerd, 1.1 41f60102fc29156bc5001d6646f75c02.cloudfront.net (CloudFront) - Etag: - - W/"d465bb01ee133f9d1241c661cd82d157" + - '1,139' + X-Ratelimit-Limit: + - '200,2000' Vary: - Accept, Origin - Status: - - 200 OK - X-Request-Id: - - bad29383-2397-4db2-932c-305d873eea17 Cache-Control: - max-age=0, private, must-revalidate Referrer-Policy: - strict-origin-when-cross-origin - X-Frame-Options: - - DENY + X-Permitted-Cross-Domain-Policies: + - none X-Xss-Protection: - 1; mode=block - X-Ratelimit-Limit: - - '600,6000' - X-Ratelimit-Usage: - - '12,12' + X-Request-Id: + - 9a14311b-d741-4a5a-abfe-f045d7611e21 + X-Readratelimit-Limit: + - '100,1000' X-Download-Options: - noopen - X-Readratelimit-Limit: - - '300,3000' + Etag: + - W/"bec055d0d5b1e69acff6ed01e62d7834" + X-Frame-Options: + - DENY X-Readratelimit-Usage: - - '12,12' + - '1,139' X-Content-Type-Options: - nosniff - X-Permitted-Cross-Domain-Policies: - - none X-Cache: - Miss from cloudfront + Via: + - 1.1 90707ba4ec932f1b72abfb5c4f1add2e.cloudfront.net (CloudFront) X-Amz-Cf-Pop: - - FRA2-C1 + - JFK52-P3 X-Amz-Cf-Id: - - 4axSzTVcQ3a-X0P71eTCZXFuqvWKSkOPsjboHqYLv4L9UdP_IJwn_g== + - "-ncC9-leXpRjwwClEYQL5TXT6p-FfJbHXD7RyqXcqa_8TjN9ydrODw==" body: encoding: UTF-8 - string: "[{\"unique_id\":\"F775717B-D1C1-443A-AD99-3D9A80FF11C9\",\"athlete_id\":24776507,\"activity_id\":3958491750,\"activity_name\":\"Bitche, - please \U0001F973\",\"post_id\":null,\"resource_state\":2,\"caption\":\"\",\"type\":1,\"source\":1,\"status\":3,\"uploaded_at\":\"2020-08-24T11:50:25Z\",\"created_at\":\"2020-08-24T09:33:05Z\",\"created_at_local\":\"2020-08-24T11:33:05Z\",\"urls\":{\"5000\":\"https://dgtzuqphqg23d.cloudfront.net/BO0H-YeNRZOfFhc0PctUheAKchsY2ll4vsagU58MNKg-2048x1536.jpg\"},\"placeholder_image\":null,\"sizes\":{\"5000\":[2048,1536]},\"default_photo\":true,\"cursor\":null,\"location\":[49.049816670000006,7.4272883300000005]}]" - recorded_at: Mon, 16 Sep 2024 09:55:25 GMT -recorded_with: VCR 6.1.0 + string: '[{"unique_id":"6D1E4A0B-0A46-406A-874A-C3ED694DDE55","athlete_id":26462176,"activity_id":16181809559,"activity_name":"Run + with Artyom","post_id":null,"resource_state":2,"caption":"","type":1,"source":1,"status":3,"uploaded_at":"2025-10-18T15:28:11Z","created_at":"2025-10-16T22:33:02Z","created_at_local":"2025-10-16T18:33:02Z","urls":{"5000":"https://dgtzuqphqg23d.cloudfront.net/gskSZVRgkpQ-O9HOdMXpYLEfDvRkZBNs_hGKBKbsmds-1536x2048.jpg"},"placeholder_image":null,"sizes":{"5000":[1536,2048]},"default_photo":true,"cursor":null,"location":[40.76128333333333,-73.97162]},{"unique_id":"5F705181-DF0D-444E-8925-87AC3DA4F983","athlete_id":26462176,"activity_id":16181809559,"activity_name":"Run + with Artyom","post_id":null,"resource_state":2,"caption":"","type":1,"source":1,"status":3,"uploaded_at":"2025-10-18T15:28:11Z","created_at":"2025-10-16T22:18:39Z","created_at_local":"2025-10-16T18:18:39Z","urls":{"5000":"https://dgtzuqphqg23d.cloudfront.net/37jS2r9iH-D8K53dVzGi6lq-Nv5JRNqpmPnO-Qdtb1s-1536x2048.jpg"},"placeholder_image":null,"sizes":{"5000":[1536,2048]},"default_photo":false,"cursor":null,"location":[40.76137166666667,-73.971825]},{"unique_id":"F5F942E4-DE41-4BFF-842B-99E66D6E4345","athlete_id":26462176,"activity_id":16181809559,"activity_name":"Run + with Artyom","post_id":null,"resource_state":2,"caption":"","type":1,"source":1,"status":3,"uploaded_at":"2025-10-18T15:28:11Z","created_at":"2025-10-16T14:07:37Z","created_at_local":"2025-10-16T10:07:37Z","urls":{"5000":"https://dgtzuqphqg23d.cloudfront.net/NSD1vutEd09T0klV_1EPJjvxkRxV2EX7PBTRc26nIQc-1536x2048.jpg"},"placeholder_image":null,"sizes":{"5000":[1536,2048]},"default_photo":false,"cursor":null,"location":[40.755705,-73.99655333333334]},{"unique_id":"077BE6E7-DDA8-4F38-BB4A-DF0F4640C2D5","athlete_id":26462176,"activity_id":16181809559,"activity_name":"Run + with Artyom","post_id":null,"resource_state":2,"caption":"","type":2,"source":1,"status":3,"uploaded_at":"2025-10-18T19:52:33Z","created_at":"2025-10-18T19:51:59Z","created_at_local":"2025-10-18T15:51:59Z","urls":{"5000":"https://d35tn3x5zm6xrc.cloudfront.net/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4/thumbnails/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4_1080x1920.jpg"},"placeholder_image":null,"sizes":{"5000":[1080,1920]},"default_photo":false,"cursor":null,"duration":4.0,"video_url":"https://d35tn3x5zm6xrc.cloudfront.net/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4/hls/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4.m3u8","location":[40.7557,-73.9966]}]' + recorded_at: Sat, 18 Oct 2025 20:08:37 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/strava/api/client/endpoints/activities/activity_photos_spec.rb b/spec/strava/api/client/endpoints/activities/activity_photos_spec.rb index 6b80272..4c158da 100644 --- a/spec/strava/api/client/endpoints/activities/activity_photos_spec.rb +++ b/spec/strava/api/client/endpoints/activities/activity_photos_spec.rb @@ -4,30 +4,66 @@ RSpec.describe 'Strava::Api::Client#activity_photos', vcr: { cassette_name: 'client/activity_photos' } do include_context 'with API client' - it 'returns activity photos' do - activity_photos = client.activity_photos(id: 7_287_327_028) - expect(activity_photos).to be_a Enumerable - expect(activity_photos.count).to eq 2 - activity_photo = activity_photos.first - expect(activity_photo.id).to be_nil - expect(activity_photo.unique_id).to eq 'f5ebd6e7-8c87-4478-86ce-ce5cf31cf519' - expect(activity_photo.urls).to eq('5000' => 'https://dgtzuqphqg23d.cloudfront.net/3wt2DyGHKHX6gJSXzpAcVdEI1QE2luP9xoDLD0CX2w4-2048x1536.jpg') - expect(activity_photo.source).to eq 1 - expect(activity_photo.athlete_id).to eq 24_776_507 - expect(activity_photo.activity_id).to eq 7_287_327_028 - expect(activity_photo.activity_name).to eq 'Die Rückkehr der Cornichons! 🥒' - expect(activity_photo.resource_state).to eq 2 - expect(activity_photo.caption).to eq '' - expect(activity_photo.created_at).to be_a Time - expect(activity_photo.created_at_local).to be_a Time - expect(activity_photo.uploaded_at).to be_a Time - expect(activity_photo.sizes).to eq('5000' => [2048, 1536]) - expect(activity_photo.default_photo).to be true + + context 'with activity photos' do + let(:activity_photos) { client.activity_photos(id: 16_181_809_559) } + + it 'returns activity photos' do + expect(activity_photos).to be_a Enumerable + expect(activity_photos.count).to eq 4 + end + + context 'when photo' do + let(:activity_photo) { activity_photos.first } + + it 'returns all fields' do + expect(activity_photo.unique_id).to eq '6D1E4A0B-0A46-406A-874A-C3ED694DDE55' + expect(activity_photo.urls).to eq('5000' => 'https://dgtzuqphqg23d.cloudfront.net/gskSZVRgkpQ-O9HOdMXpYLEfDvRkZBNs_hGKBKbsmds-1536x2048.jpg') + expect(activity_photo.source).to eq 1 + expect(activity_photo.status).to eq 3 + expect(activity_photo.cursor).to be_nil + expect(activity_photo.athlete_id).to eq 26_462_176 + expect(activity_photo.activity_id).to eq 16_181_809_559 + expect(activity_photo.activity_name).to eq 'Run with Artyom' + expect(activity_photo.resource_state).to eq 2 + expect(activity_photo.caption).to eq '' + expect(activity_photo.created_at).to be_a Time + expect(activity_photo.created_at_local).to be_a Time + expect(activity_photo.uploaded_at).to be_a Time + expect(activity_photo.sizes).to eq('5000' => [1536, 2048]) + expect(activity_photo.default_photo).to be true + end + end + + context 'when video' do + let(:activity_video) { activity_photos[3] } + + it 'returns all fields' do + expect(activity_video.unique_id).to eq '077BE6E7-DDA8-4F38-BB4A-DF0F4640C2D5' + expect(activity_video.urls).to eq('5000' => 'https://d35tn3x5zm6xrc.cloudfront.net/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4/thumbnails/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4_1080x1920.jpg') + expect(activity_video.source).to eq 1 + expect(activity_video.status).to eq 3 + expect(activity_video.cursor).to be_nil + expect(activity_video.athlete_id).to eq 26_462_176 + expect(activity_video.activity_id).to eq 16_181_809_559 + expect(activity_video.activity_name).to eq 'Run with Artyom' + expect(activity_video.resource_state).to eq 2 + expect(activity_video.caption).to eq '' + expect(activity_video.created_at).to be_a Time + expect(activity_video.created_at_local).to be_a Time + expect(activity_video.uploaded_at).to be_a Time + expect(activity_video.sizes).to eq('5000' => [1080, 1920]) + expect(activity_video.default_photo).to be false + expect(activity_video.video_url).to eq 'https://d35tn3x5zm6xrc.cloudfront.net/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4/hls/LEpTlUieI8wxuKwsdTd_CAHz0i2BccppnSXEfVV5_p4.m3u8' + expect(activity_video.duration).to eq 4 + expect(activity_video.location).to eq([40.7557, -73.9966]) + end + end end it 'returns activity photos by id' do - activity_photos = client.activity_photos(3_958_491_750) + activity_photos = client.activity_photos(16_181_809_559) expect(activity_photos).to be_a Enumerable - expect(activity_photos.count).to eq 1 + expect(activity_photos.count).to eq 4 end end diff --git a/spec/strava/api/client/endpoints/activities/activity_ride_spec.rb b/spec/strava/api/client/endpoints/activities/activity_ride_spec.rb index 465e0bf..690ad3c 100644 --- a/spec/strava/api/client/endpoints/activities/activity_ride_spec.rb +++ b/spec/strava/api/client/endpoints/activities/activity_ride_spec.rb @@ -227,7 +227,6 @@ expect(photos.count).to eq 1 photo = photos.primary expect(photo).to be_a Strava::Models::Photo - expect(photo.id).to be_nil expect(photo.unique_id).to eq 'F775717B-D1C1-443A-AD99-3D9A80FF11C9' expect(photo.urls).to eq( '100' => 'https://dgtzuqphqg23d.cloudfront.net/BO0H-YeNRZOfFhc0PctUheAKchsY2ll4vsagU58MNKg-128x96.jpg', diff --git a/spec/strava/api/client/endpoints/activities/activity_spec.rb b/spec/strava/api/client/endpoints/activities/activity_spec.rb index 8586630..1cff627 100644 --- a/spec/strava/api/client/endpoints/activities/activity_spec.rb +++ b/spec/strava/api/client/endpoints/activities/activity_spec.rb @@ -244,7 +244,6 @@ expect(photos.count).to eq 9 photo = photos.primary expect(photo).to be_a Strava::Models::Photo - expect(photo.id).to be_nil expect(photo.unique_id).to eq '5e8006d0-8349-40ad-a4ef-72b5e6e82dfe' expect(photo.urls).to eq( '100' => 'https://dgtzuqphqg23d.cloudfront.net/mo8thQ4Z5qAylUaRZHOWAR1sp16Bo-pp0ggYQKSWiZE-90x128.jpg', diff --git a/spec/strava/webhooks/client_spec.rb b/spec/strava/webhooks/client_spec.rb index ddd97cc..351adf7 100644 --- a/spec/strava/webhooks/client_spec.rb +++ b/spec/strava/webhooks/client_spec.rb @@ -64,7 +64,7 @@ end context 'with a client id and secret' do - let(:client) { described_class.new(client_id: ENV.fetch('STRAVA_CLIENT_ID', '24523'), client_secret: ENV.fetch('STRAVA_CLIENT_SECRET', 'client-secret')) } + let(:client) { described_class.new(client_id: '24523', client_secret: 'client-secret') } describe '#push_subscriptions' do it 'gets an empty set of push subscriptions', vcr: { cassette_name: 'webhooks/no_push_subscriptions' } do diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb index 59a6e5a..8f6175e 100644 --- a/spec/support/vcr.rb +++ b/spec/support/vcr.rb @@ -6,7 +6,7 @@ VCR.configure do |config| config.cassette_library_dir = 'spec/fixtures/strava' config.hook_into :webmock - # config.default_cassette_options = { record: :new_episodes } + config.default_cassette_options = { record: :new_episodes } config.configure_rspec_metadata! config.before_record do |i| i.request.headers['Authorization'] = ['Bearer access-token'] if ENV.key?('STRAVA_ACCESS_TOKEN')