From 72acac072c700d104c1b4b59e988804a1e22b122 Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Sat, 13 Sep 2025 16:04:48 +0200 Subject: [PATCH] fixes strava integration --- db/integrations/strava/models.go | 1 + db/integrations/strava/strava.go | 70 +++++++++++-------- .../integrations/strava_settings_modal.svelte | 37 +++++++++- web/src/lib/i18n/locales/de.json | 3 +- web/src/lib/i18n/locales/en.json | 3 +- web/src/lib/i18n/locales/es.json | 3 +- web/src/lib/i18n/locales/eu.json | 4 +- web/src/lib/i18n/locales/fr.json | 3 +- web/src/lib/i18n/locales/hu.json | 3 +- web/src/lib/i18n/locales/it.json | 3 +- web/src/lib/i18n/locales/nl.json | 3 +- web/src/lib/i18n/locales/pl.json | 3 +- web/src/lib/i18n/locales/pt.json | 3 +- web/src/lib/i18n/locales/ru.json | 6 +- web/src/lib/i18n/locales/zh.json | 3 +- web/src/lib/models/api/integration_schema.ts | 3 +- web/src/lib/models/integration.ts | 1 + 17 files changed, 102 insertions(+), 50 deletions(-) diff --git a/db/integrations/strava/models.go b/db/integrations/strava/models.go index 6b8fead8..8fa4f0fd 100644 --- a/db/integrations/strava/models.go +++ b/db/integrations/strava/models.go @@ -29,6 +29,7 @@ type StravaIntegration struct { AccessToken string `json:"accessToken,omitempty"` RefreshToken string `json:"refreshToken,omitempty"` ExpiresAt int64 `json:"expiresAt,omitempty"` + After string `json:"after,omitempty"` } type StravaRoute struct { Athlete Athlete `json:"athlete"` diff --git a/db/integrations/strava/strava.go b/db/integrations/strava/strava.go index 974af4f0..bf347381 100644 --- a/db/integrations/strava/strava.go +++ b/db/integrations/strava/strava.go @@ -89,22 +89,12 @@ func SyncStrava(app core.App) error { stravaIntegration.ExpiresAt = r.ExpiresAt } - b, err := json.Marshal(stravaIntegration) - if err != nil { - return err - } - i.Set("strava", string(b)) - err = app.Save(i) - if err != nil { - return err - } - if stravaIntegration.Routes { page := 1 - hasNewRoutes := true - for hasNewRoutes { - + hasMore := true + for hasMore { routes, err := fetchStravaRoutes(r.AccessToken, page) + hasMore = len(routes) > 0 page += 1 if err != nil { warning := fmt.Sprintf("error fetching routes from strava: %v\n", err) @@ -112,7 +102,7 @@ func SyncStrava(app core.App) error { app.Logger().Warn(warning) break } - hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes) + err = syncTrailsWithRoutes(app, r.AccessToken, userId, actorId, routes) if err != nil { warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err) fmt.Print(warning) @@ -123,9 +113,20 @@ func SyncStrava(app core.App) error { } if stravaIntegration.Activities { page := 1 - hasNewActivities := true - for hasNewActivities { - activities, err := fetchStravaActivities(r.AccessToken, page) + hasMore := true + for hasMore { + var after int64 = 0 + if stravaIntegration.After != "" { + t, err := time.Parse("2006-01-02", stravaIntegration.After) + if err != nil { + return err + } + t = t.UTC() + + after = t.Unix() + } + activities, err := fetchStravaActivities(r.AccessToken, page, after) + hasMore = len(activities) > 0 page += 1 if err != nil { warning := fmt.Sprintf("error fetching activities from strava: %v", err) @@ -133,7 +134,8 @@ func SyncStrava(app core.App) error { app.Logger().Warn(warning) break } - hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, actorId, activities) + err = syncTrailsWithActivities(app, r.AccessToken, actorId, activities) + if err != nil { warning := fmt.Sprintf("error syncing strava activities with trails: %v", err) fmt.Print(warning) @@ -141,6 +143,17 @@ func SyncStrava(app core.App) error { continue } } + + } + + b, err := json.Marshal(stravaIntegration) + if err != nil { + return err + } + i.Set("strava", string(b)) + err = app.Save(i) + if err != nil { + return err } } @@ -208,8 +221,8 @@ func fetchStravaRoutes(accessToken string, page int) ([]StravaRoute, error) { return routes, nil } -func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, error) { - stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d", page) +func fetchStravaActivities(accessToken string, page int, after int64) ([]StravaActivity, error) { + stravaRoutesURL := fmt.Sprintf("https://www.strava.com/api/v3/athlete/activities?page=%d&after=%d", page, after) req, err := http.NewRequest("GET", stravaRoutesURL, nil) if err != nil { return nil, err @@ -235,17 +248,15 @@ func fetchStravaActivities(accessToken string, page int) ([]StravaActivity, erro return activities, nil } -func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) (bool, error) { - hasNewRoutes := false +func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor string, routes []StravaRoute) error { for _, route := range routes { trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": route.IDStr}) if err != nil { - return hasNewRoutes, err + return err } if len(trails) != 0 { continue } - hasNewRoutes = true gpx, err := fetchRouteGPX(route, accessToken) if err != nil { app.Logger().Warn(fmt.Sprintf("Unable to fetch GPX for route '%s': %v", route.Name, err)) @@ -263,7 +274,7 @@ func syncTrailsWithRoutes(app core.App, accessToken string, user string, actor s } } - return hasNewRoutes, nil + return nil } func fetchRouteGPX(route StravaRoute, accessToken string) (*filesystem.File, error) { @@ -393,17 +404,15 @@ func createWaypointsFromRoute(app core.App, route StravaRoute, user string, trai return nil } -func syncTrailsWithActivities(app core.App, accessToken string, user string, actor string, activities []StravaActivity) (bool, error) { - hasNewActivites := false +func syncTrailsWithActivities(app core.App, accessToken string, actor string, activities []StravaActivity) error { for _, activity := range activities { trails, err := app.FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(activity.ID))}) if err != nil { - return hasNewActivites, err + return err } if len(trails) != 0 { continue } - hasNewActivites = true detailedActivity, err := fetchDetailedActivity(activity, accessToken) if err != nil { app.Logger().Warn(fmt.Sprintf("Unable to fetch detailed activity '%s': %v", activity.Name, err)) @@ -419,10 +428,9 @@ func syncTrailsWithActivities(app core.App, accessToken string, user string, act app.Logger().Warn(fmt.Sprintf("Unable to create trail from activity '%s': %v", activity.Name, err)) continue } - } - return hasNewActivites, nil + return nil } func fetchDetailedActivity(activity StravaActivity, accessToken string) (*DetailedStravaActivity, error) { diff --git a/web/src/lib/components/settings/integrations/strava_settings_modal.svelte b/web/src/lib/components/settings/integrations/strava_settings_modal.svelte index b9a3c822..601d6afb 100644 --- a/web/src/lib/components/settings/integrations/strava_settings_modal.svelte +++ b/web/src/lib/components/settings/integrations/strava_settings_modal.svelte @@ -1,4 +1,5 @@ +

+ {$_("strava-integration-after-date-hint")} +

+
+ + +
{/snippet} {#snippet footer()} diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json index cb5545be..1e4ff9a3 100644 --- a/web/src/lib/i18n/locales/de.json +++ b/web/src/lib/i18n/locales/de.json @@ -32,6 +32,7 @@ "bakery": "Bäckerei", "barrier": "Barriere", "basic-info": "Basisinformation", + "basque": "Baskisch", "before": "Vor", "bicycle-parking": "Fahrrad Parkplatz", "bicycle-rental": "Fahrradverleih", @@ -56,7 +57,6 @@ "car": "Auto", "car-motorcycle": "Auto/Motorrad", "card": "{n, plural, =1 {Karte} other {Karten}}", - "basque": "Baskisch", "categories": "Kategorien", "category": "Kategorie", "change": "Ändern", @@ -390,6 +390,7 @@ "statistics": "Statistiken", "stop-drawing": "Zeichnen beenden", "stop-editing": "Bearbeiten beenden", + "strava-integration-after-date-hint": "", "subway-stop": "U-Bahn Eingang", "summit": "Gipfel", "summit-book": "Gipfelbuch", diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index e8650b59..c3171a98 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Basic Info", + "basque": "Basque", "before": "Before", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Car", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Card} other {Cards}}", - "basque": "Basque", "categories": "Categories", "category": "Category", "change": "Change", @@ -390,6 +390,7 @@ "statistics": "Statistics", "stop-drawing": "Stop drawing", "stop-editing": "Stop editing", + "strava-integration-after-date-hint": "If your account has a large amount of acitivities you may run into Strava's API rate limit preventing you from syncing all activities at once. To mitigate this issue you can set an \"After\" date below so that only activities that were recorded after this date are synced.", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Summit Book", diff --git a/web/src/lib/i18n/locales/es.json b/web/src/lib/i18n/locales/es.json index 11b4f976..2678c064 100644 --- a/web/src/lib/i18n/locales/es.json +++ b/web/src/lib/i18n/locales/es.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Información básica", + "basque": "Basque", "before": "Antes", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Coche", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, one {}=1 {Ficha} other {Fichas}}", - "basque": "Basque", "categories": "Categorías", "category": "Categoría", "change": "Modificar", @@ -390,6 +390,7 @@ "statistics": "Estadísticas", "stop-drawing": "Parar de diseñar", "stop-editing": "Parar de editar", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Libro de ascensos", diff --git a/web/src/lib/i18n/locales/eu.json b/web/src/lib/i18n/locales/eu.json index 3d069b2b..f1746b53 100644 --- a/web/src/lib/i18n/locales/eu.json +++ b/web/src/lib/i18n/locales/eu.json @@ -32,6 +32,7 @@ "bakery": "Okindegia", "barrier": "Oztopoa", "basic-info": "Oinarrizko informazioa", + "basque": "Basque", "before": "Aurretik", "bicycle-parking": "Bizikleta-parkina", "bicycle-rental": "Bizikleta-alokairua", @@ -56,7 +57,6 @@ "car": "Kotxea", "car-motorcycle": "Kotxea/Motozikleta", "card": "{n, plural, one {}=1 {Txartea} other {Txartelak}}", - "basque": "Basque", "categories": "Kategoriak", "category": "Kategoria", "change": "Aldatu", @@ -338,7 +338,6 @@ "route-point": "Ibilbideko puntua", "russian": "Russian", "save": "Gorde", - "russian": "Russian", "save-list": "Gorde zerrenda", "save-trail": "Gorde ibilaldia", "save-your-trail-first": "Gorde zure ibilaldia lehenengo", @@ -391,6 +390,7 @@ "statistics": "Estatistikak", "stop-drawing": "Utzi marrazteari", "stop-editing": "Utzi editatzeari", + "strava-integration-after-date-hint": "", "subway-stop": "Metro sarbidea", "summit": "Gailurra", "summit-book": "Igoeren liburua", diff --git a/web/src/lib/i18n/locales/fr.json b/web/src/lib/i18n/locales/fr.json index 7f4dd481..35f74061 100644 --- a/web/src/lib/i18n/locales/fr.json +++ b/web/src/lib/i18n/locales/fr.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Informations de base", + "basque": "Basque", "before": "Avant le", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Voiture", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Tuile} other {Tuiles}}", - "basque": "Basque", "categories": "Catégories", "category": "Catégorie", "change": "Changement", @@ -390,6 +390,7 @@ "statistics": "Statistiques", "stop-drawing": "Arrêter de tracer", "stop-editing": "Arrêter la modification", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Liste des ascensions", diff --git a/web/src/lib/i18n/locales/hu.json b/web/src/lib/i18n/locales/hu.json index da62034c..f5ac3a50 100644 --- a/web/src/lib/i18n/locales/hu.json +++ b/web/src/lib/i18n/locales/hu.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Alap információk", + "basque": "Basque", "before": "Before", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Car", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Kártya} other {Kártyák}}", - "basque": "Basque", "categories": "Kategóriák", "category": "Kategória", "change": "Változás", @@ -390,6 +390,7 @@ "statistics": "Statistics", "stop-drawing": "Stop drawing", "stop-editing": "Stop editing", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Csúcspont könyv", diff --git a/web/src/lib/i18n/locales/it.json b/web/src/lib/i18n/locales/it.json index bdbcf06b..6c5926d3 100644 --- a/web/src/lib/i18n/locales/it.json +++ b/web/src/lib/i18n/locales/it.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Informazioni di base", + "basque": "Basque", "before": "Prima", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Car", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Carta} other {Carte}}", - "basque": "Basque", "categories": "Categorie", "category": "Categoria", "change": "Modifica", @@ -390,6 +390,7 @@ "statistics": "Statistiche", "stop-drawing": "Smettere di disegnare", "stop-editing": "Stop editing", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Libro di vetta", diff --git a/web/src/lib/i18n/locales/nl.json b/web/src/lib/i18n/locales/nl.json index 995aefde..56044cb5 100644 --- a/web/src/lib/i18n/locales/nl.json +++ b/web/src/lib/i18n/locales/nl.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Algemene informatie", + "basque": "Basque", "before": "Voor", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Wagen", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Kaart} other {Kaarten}}", - "basque": "Basque", "categories": "Categorieën", "category": "Categorie", "change": "Wijzigen", @@ -390,6 +390,7 @@ "statistics": "Statistieken", "stop-drawing": "Stop met tekenen", "stop-editing": "Stop met bewerken", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Bergtopboek", diff --git a/web/src/lib/i18n/locales/pl.json b/web/src/lib/i18n/locales/pl.json index a769c7f4..517d1234 100644 --- a/web/src/lib/i18n/locales/pl.json +++ b/web/src/lib/i18n/locales/pl.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Podstawowe informacje", + "basque": "Basque", "before": "Przed", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Samochód", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Karta} other {Karty}}", - "basque": "Basque", "categories": "Kategorie", "category": "Kategoria", "change": "Zmień", @@ -390,6 +390,7 @@ "statistics": "Statystyki", "stop-drawing": "Przestań rysować", "stop-editing": "Zakończ edycję", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Logbook", diff --git a/web/src/lib/i18n/locales/pt.json b/web/src/lib/i18n/locales/pt.json index ac224ec5..b4815750 100644 --- a/web/src/lib/i18n/locales/pt.json +++ b/web/src/lib/i18n/locales/pt.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Informações básicas", + "basque": "Basque", "before": "Antes", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Car", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Cartão} other {Cartões}}", - "basque": "Basque", "categories": "Categorias", "category": "Categoria", "change": "Alterar", @@ -390,6 +390,7 @@ "statistics": "Statistics", "stop-drawing": "Parar desenho", "stop-editing": "Stop editing", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "Livro da cimeira", diff --git a/web/src/lib/i18n/locales/ru.json b/web/src/lib/i18n/locales/ru.json index 9865603e..398e4800 100644 --- a/web/src/lib/i18n/locales/ru.json +++ b/web/src/lib/i18n/locales/ru.json @@ -32,6 +32,7 @@ "bakery": "Bakery", "barrier": "Barrier", "basic-info": "Основная информация", + "basque": "Basque", "before": "До", "bicycle-parking": "Bicycle Parking", "bicycle-rental": "Bicycle Rental", @@ -56,7 +57,6 @@ "car": "Автомобиль:", "car-motorcycle": "Car/Motorcycle", "card": "{n, plural, =1 {Карточка} other {Карточки}}", - "basque": "Basque", "categories": "Категории", "category": "Категория", "change": "Изменить", @@ -339,7 +339,6 @@ "russian": "Russian", "save": "Сохранить", "save-list": "Сохранить список", - "russian": "Russian", "save-trail": "Сохранить трек", "save-your-trail-first": "Сначала сохраните трек", "search-cities": "Поиск по городам", @@ -391,6 +390,7 @@ "statistics": "Статистика", "stop-drawing": "Закончить рисование", "stop-editing": "Закончить редактирование", + "strava-integration-after-date-hint": "", "subway-stop": "Subway entrance", "summit": "Summit", "summit-book": "История поездок", @@ -426,4 +426,4 @@ "width": "Ширина", "wrong-username-or-password": "Неверный логин или пароль", "you-can": "Вы можете" -} \ No newline at end of file +} diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json index 50dce55a..e5b836d5 100644 --- a/web/src/lib/i18n/locales/zh.json +++ b/web/src/lib/i18n/locales/zh.json @@ -32,6 +32,7 @@ "bakery": "面包店", "barrier": "障碍", "basic-info": "基本信息", + "basque": "Basque", "before": "之前", "bicycle-parking": "自行车停车场", "bicycle-rental": "自行车租车", @@ -56,7 +57,6 @@ "car": "汽车", "car-motorcycle": "汽车/摩托车", "card": "{n, plural, =1 {卡片} other {卡片}}", - "basque": "Basque", "categories": "分类", "category": "分类", "change": "更换", @@ -390,6 +390,7 @@ "statistics": "统计", "stop-drawing": "停止绘制", "stop-editing": "停止编辑", + "strava-integration-after-date-hint": "", "subway-stop": "地铁入口", "summit": "山峰", "summit-book": "详细日程", diff --git a/web/src/lib/models/api/integration_schema.ts b/web/src/lib/models/api/integration_schema.ts index 6895e341..fce918d3 100644 --- a/web/src/lib/models/api/integration_schema.ts +++ b/web/src/lib/models/api/integration_schema.ts @@ -6,7 +6,8 @@ const StravaSchema = z.object({ clientSecret: z.string().length(40).optional().or(z.literal('')), routes: z.boolean(), activities: z.boolean(), - active: z.boolean() + active: z.boolean(), + after: z.string().date().optional(), }) const KomootSchema = z.object({ diff --git a/web/src/lib/models/integration.ts b/web/src/lib/models/integration.ts index 91a587e2..ca845c8d 100644 --- a/web/src/lib/models/integration.ts +++ b/web/src/lib/models/integration.ts @@ -11,6 +11,7 @@ export interface StravaIntegration extends BaseIntegration { accessToken?: string; refreshToken?: string; expiresAt?: number; + after?: string } export interface KomootIntegration extends BaseIntegration {