{$_("integrations")}
-
strava
-- Syncs your strava routes with wanderer in regular intervals. -
-diff --git a/db/integrations/komoot/komoot.go b/db/integrations/komoot/komoot.go new file mode 100644 index 00000000..01656a7b --- /dev/null +++ b/db/integrations/komoot/komoot.go @@ -0,0 +1,371 @@ +package komoot + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/twpayne/go-gpx" +) + +func SyncKomoot(app *pocketbase.PocketBase) error { + integrations, err := app.Dao().FindRecordsByExpr("integrations", dbx.NewExp("true")) + if err != nil { + return err + } + + for _, i := range integrations { + userId := i.GetString("user") + komootString := i.GetString("komoot") + var komootIntegration KomootIntegration + json.Unmarshal([]byte(komootString), &komootIntegration) + + if !komootIntegration.Active || komootIntegration.Email == "" || komootIntegration.Password == "" { + continue + } + k := &KomootApi{} + err = k.login(komootIntegration.Email, komootIntegration.Password) + if err != nil { + warning := fmt.Sprintf("komoot login failed: %v\n", err) + fmt.Print(warning) + app.Logger().Warn(warning) + continue + } + hasNewTours := true + page := 0 + for hasNewTours { + tours, err := k.fetchTours(page) + if err != nil { + warning := fmt.Sprintf("error fetching tours from komoot: %v\n", err) + fmt.Print(warning) + app.Logger().Warn(warning) + break + } + + hasNewTours, err = syncTrailWithTours(app, k, userId, tours) + if err != nil { + warning := fmt.Sprintf("error syncing komoot tours with trails: %v\n", err) + fmt.Print(warning) + app.Logger().Warn(warning) + break + } + page += 1 + } + } + + return nil +} + +type BasicAuthToken struct { + Key string + Value string +} + +func (b BasicAuthToken) Apply(req *http.Request) { + authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(b.Key+":"+b.Value)) + req.Header.Set("Authorization", authStr) +} + +type KomootApi struct { + UserID string + Token string +} + +func (k *KomootApi) buildHeader() *BasicAuthToken { + if k.UserID != "" && k.Token != "" { + return &BasicAuthToken{k.UserID, k.Token} + } + return nil +} + +func sendRequest(url string, auth *BasicAuthToken) ([]byte, error) { + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + if auth != nil { + auth.Apply(req) + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("error sending request to komoot (%d): %s", resp.StatusCode, string(body)) + } + + return io.ReadAll(resp.Body) +} + +func (k *KomootApi) login(email, password string) error { + url := fmt.Sprintf("https://api.komoot.de/v006/account/email/%s/", email) + + body, err := sendRequest(url, &BasicAuthToken{email, password}) + if err != nil { + return err + } + + var data LoginResponse + json.Unmarshal(body, &data) + + k.UserID = data.Username + k.Token = data.Password + + return nil +} +func (k *KomootApi) fetchTours(page int) ([]KomootTour, error) { + currentUri := fmt.Sprintf("https://api.komoot.de/v007/users/%s/tours/?page=%d&sort_field=date&sort_direction=desc&limit=30", k.UserID, page) + + body, err := sendRequest(currentUri, k.buildHeader()) + if err != nil { + return nil, err + } + + var data KomootToursResponse + json.Unmarshal(body, &data) + + tours := data.Embedded.Tours + + return tours, nil +} + +func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, error) { + url := fmt.Sprintf("https://api.komoot.de/v007/tours/%d?_embedded=coordinates,way_types,surfaces,directions,participants,timeline&directions=v2&fields=timeline&format=coordinate_array&timeline_highlights_fields=tips,recommenders", tour.ID) + body, err := sendRequest(url, k.buildHeader()) + if err != nil { + return nil, err + } + + var data *DetailedKomootTour + json.Unmarshal(body, &data) + return data, nil +} + +func syncTrailWithTours(app *pocketbase.PocketBase, k *KomootApi, user string, tours []KomootTour) (bool, error) { + hasNewTours := false + for _, tour := range tours { + trails, err := app.Dao().FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(tour.ID))}) + if err != nil { + return hasNewTours, err + } + if len(trails) != 0 { + continue + } + hasNewTours = true + detailedTour, err := k.fetchDetailedTour(tour) + if err != nil { + return hasNewTours, err + } + gpx, err := generateTourGPX(detailedTour) + if err != nil { + return hasNewTours, err + } + wpIds, err := createWaypointsFromTour(app, detailedTour, user) + if err != nil { + return hasNewTours, err + } + err = createTrailFromTour(app, detailedTour, gpx, user, wpIds) + if err != nil { + return hasNewTours, err + } + + } + return hasNewTours, nil +} + +func createTrailFromTour(app *pocketbase.PocketBase, detailedTour *DetailedKomootTour, gpx *filesystem.File, user string, wpIds []string) error { + collection, err := app.Dao().FindCollectionByNameOrId("trails") + if err != nil { + return err + } + + record := models.NewRecord(collection) + + form := forms.NewRecordUpsert(app, record) + + categoryMap := map[string]string{ + "hike": "Hiking", + "touringbicycle": "Biking", + "mtb": "Biking", + "racebike": "Biking", + "jogging": "Walking", + "mtb_easy": "Workout", + "mtb_advanced": "Walking", + "mountaineering": "Hiking", + } + + category, _ := app.Dao().FindFirstRecordByData("categories", "name", categoryMap[detailedTour.Sport]) + categoryId := "" + if category != nil { + categoryId = category.Id + } + + photo, err := fetchPhoto(detailedTour.MapImage.Src, "", "") + if err != nil { + return err + } + + form.LoadData(map[string]any{ + "name": detailedTour.Name, + "public": detailedTour.Status == "public", + "distance": detailedTour.Distance, + "elevation_gain": detailedTour.ElevationUp, + "elevation_loss": detailedTour.ElevationDown, + "duration": detailedTour.Duration / 60, + "date": detailedTour.Date, + "external_provider": "komoot", + "external_id": strconv.Itoa(detailedTour.ID), + "lat": detailedTour.StartPoint.Lat, + "lon": detailedTour.StartPoint.Lng, + "difficulty": detailedTour.Difficulty.Grade, + "category": categoryId, + "waypoints": wpIds, + "author": user, + }) + + form.AddFiles("photos", photo) + form.AddFiles("gpx", gpx) + + if err := form.Submit(); err != nil { + return err + } + + return nil +} + +func createWaypointsFromTour(app *pocketbase.PocketBase, tour *DetailedKomootTour, user string) ([]string, error) { + collection, err := app.Dao().FindCollectionByNameOrId("waypoints") + if err != nil { + return nil, err + } + + wpIds := make([]string, len(tour.Embedded.Timeline.Embedded.Items)) + + for i, wp := range tour.Embedded.Timeline.Embedded.Items { + photos, err := fetchWaypointPhotos(wp) + if err != nil { + return nil, err + } + record := models.NewRecord(collection) + form := forms.NewRecordUpsert(app, record) + + wpDescription := "" + if len(wp.Embedded.Reference.Embedded.Tips.Embedded.Items) > 0 { + wpDescription = wp.Embedded.Reference.Embedded.Tips.Embedded.Items[0].Text + } + + wpLat := wp.Embedded.Reference.StartPoint.Lat + if wpLat == 0 { + wpLat = tour.StartPoint.Lat + } + + wpLon := wp.Embedded.Reference.StartPoint.Lng + if wpLon == 0 { + wpLon = tour.StartPoint.Lng + } + + form.LoadData(map[string]any{ + "name": wp.Embedded.Reference.Name, + "description": wpDescription, + "lat": wpLat, + "lon": wpLon, + "icon": "circle", + "author": user, + "distance_from_start": 0, + }) + + for _, photo := range photos { + form.AddFiles("photos", photo) + } + + if err := form.Submit(); err != nil { + return nil, err + } + + wpIds[i] = record.Id + } + + return wpIds, nil +} + +func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) { + + photos := make([]*filesystem.File, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items)) + + for i, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items { + photo, err := fetchPhoto(img.Src, "", "") + if err != nil { + return nil, err + } + photos[i] = photo + } + + return photos, nil +} + +func fetchPhoto(url string, width string, height string) (*filesystem.File, error) { + url = strings.Replace(url, "{crop}", "false", 1) + url = strings.Replace(url, "{width}", width, 1) + url = strings.Replace(url, "{height}", height, 1) + + bytes, err := sendRequest(url, nil) + if err != nil { + return nil, err + } + + return filesystem.NewFileFromBytes(bytes, "photo") +} + +func generateTourGPX(detailedTour *DetailedKomootTour) (*filesystem.File, error) { + var points []*gpx.WptType + + for _, item := range detailedTour.Embedded.Coordinates.Items { + t := detailedTour.Date.Unix() + int64(item.T) + + points = append(points, &gpx.WptType{Lat: item.Lat, Lon: item.Lng, Ele: item.Alt, Time: time.Unix(t, 0)}) + } + + gpx := &gpx.GPX{ + Version: "1.1", + Creator: "Strava GPX Exporter", + Trk: []*gpx.TrkType{ + { + Name: detailedTour.Name, + TrkSeg: []*gpx.TrkSegType{ + { + TrkPt: points, + }, + }, + }, + }, + } + var buf bytes.Buffer + err := gpx.Write(&buf) + if err != nil { + return nil, err + } + + gpxFile, err := filesystem.NewFileFromBytes(buf.Bytes(), detailedTour.Name+".gpx") + if err != nil { + return nil, err + } + + return gpxFile, nil +} diff --git a/db/integrations/komoot/models.go b/db/integrations/komoot/models.go new file mode 100644 index 00000000..3d62d41c --- /dev/null +++ b/db/integrations/komoot/models.go @@ -0,0 +1,379 @@ +package komoot + +import "time" + +type KomootIntegration struct { + Active bool `json:"active"` + Email string `json:"email"` + Password string `json:"password"` +} + +type LoginResponse struct { + Email string `json:"email"` + Password string `json:"password"` + User User `json:"user"` + Username string `json:"username"` +} + +type Content struct { + HasImage bool `json:"hasImage"` +} + +type Fitness struct { + Personalised bool `json:"personalised"` +} + +type User struct { + Content Content `json:"content"` + CreatedAt string `json:"createdAt"` + Displayname string `json:"displayname"` + Fitness Fitness `json:"fitness"` + ImageURL string `json:"imageUrl"` + Locale string `json:"locale"` + Metric bool `json:"metric"` + Newsletter bool `json:"newsletter"` + State string `json:"state"` + Username string `json:"username"` + WelcomeMails bool `json:"welcomeMails"` +} + +type KomootToursResponse struct { + Embedded Embedded `json:"_embedded"` + Links ResponseLinks `json:"_links"` + Page Page `json:"page"` +} +type StartPoint struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + Alt float64 `json:"alt"` +} +type Surfaces struct { + Type string `json:"type"` + Amount float64 `json:"amount"` +} +type WayTypes struct { + Type string `json:"type"` + Amount float64 `json:"amount"` +} +type Summary struct { + Surfaces []Surfaces `json:"surfaces"` + WayTypes []WayTypes `json:"way_types"` +} +type Difficulty struct { + Grade string `json:"grade"` + ExplanationTechnical string `json:"explanation_technical"` + ExplanationFitness string `json:"explanation_fitness"` +} +type Location struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` +} +type Path struct { + Location Location `json:"location"` + Index int `json:"index"` + Reference string `json:"reference,omitempty"` + EndIndex int `json:"end_index,omitempty"` + SegmentType string `json:"segment_type,omitempty"` +} +type Segments struct { + Type string `json:"type"` + From int `json:"from"` + To int `json:"to"` +} +type MapImage struct { + Src string `json:"src"` + Templated bool `json:"templated"` + Type string `json:"type"` + Attribution string `json:"attribution"` +} +type MapImagePreview struct { + Src string `json:"src"` + Templated bool `json:"templated"` + Type string `json:"type"` + Attribution string `json:"attribution"` +} +type VectorMapImage struct { + Src string `json:"src"` + Templated bool `json:"templated"` + Type string `json:"type"` + Attribution string `json:"attribution"` +} +type VectorMapImagePreview struct { + Src string `json:"src"` + Templated bool `json:"templated"` + Type string `json:"type"` + Attribution string `json:"attribution"` +} + +type Relation struct { + Href string `json:"href"` + Templated bool `json:"templated"` +} +type CreatorLinks struct { + Relation Relation `json:"relation"` +} + +type LinksEmbedded struct { + Creator Creator `json:"creator"` +} +type LinksCreator struct { + Href string `json:"href"` +} +type LinksCoordinates struct { + Href string `json:"href"` +} +type LinksTourLine struct { + Href string `json:"href"` +} +type LinksParticipants struct { + Href string `json:"href"` +} +type LinksWayTypes struct { + Href string `json:"href"` +} +type LinksSurfaces struct { + Href string `json:"href"` +} +type LinksDirections struct { + Href string `json:"href"` +} +type LinksTimeline struct { + Href string `json:"href"` +} +type LinksTranslations struct { + Href string `json:"href"` +} +type LinksCoverImages struct { + Href string `json:"href"` +} +type LinksTourRating struct { + Href string `json:"href"` +} +type TourLinks struct { + Creator LinksCreator `json:"creator"` + Coordinates LinksCoordinates `json:"coordinates"` + TourLine LinksTourLine `json:"tour_line"` + Participants LinksParticipants `json:"participants"` + WayTypes LinksWayTypes `json:"way_types"` + Surfaces LinksSurfaces `json:"surfaces"` + Directions LinksDirections `json:"directions"` + Timeline LinksTimeline `json:"timeline"` + Translations LinksTranslations `json:"translations"` + CoverImages LinksCoverImages `json:"cover_images"` + TourRating LinksTourRating `json:"tour_rating"` +} +type KomootTour struct { + ID int `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Source string `json:"source"` + RoutingVersion string `json:"routing_version"` + Status string `json:"status"` + Date time.Time `json:"date"` + KcalActive int `json:"kcal_active"` + KcalResting int `json:"kcal_resting"` + StartPoint StartPoint `json:"start_point"` + Distance float64 `json:"distance"` + Duration int `json:"duration"` + ElevationUp float64 `json:"elevation_up"` + ElevationDown float64 `json:"elevation_down"` + Sport string `json:"sport"` + Query string `json:"query"` + Constitution int `json:"constitution"` + Summary Summary `json:"summary"` + Difficulty Difficulty `json:"difficulty"` + TourInformation []any `json:"tour_information"` + Path []Path `json:"path"` + Segments []Segments `json:"segments"` + ChangedAt time.Time `json:"changed_at"` + MapImage MapImage `json:"map_image"` + MapImagePreview MapImagePreview `json:"map_image_preview"` + VectorMapImage VectorMapImage `json:"vector_map_image"` + VectorMapImagePreview VectorMapImagePreview `json:"vector_map_image_preview"` + PotentialRouteUpdate bool `json:"potential_route_update"` + Embedded Embedded `json:"_embedded"` + Links TourLinks `json:"_links"` +} +type Embedded struct { + Tours []KomootTour `json:"tours"` +} +type Next struct { + Href string `json:"href"` +} +type ResponseLinks struct { + Next Next `json:"next"` +} +type Page struct { + Size int `json:"size"` + TotalElements int `json:"totalElements"` + TotalPages int `json:"totalPages"` + Number int `json:"number"` +} + +type DetailedKomootTour struct { + ID int `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Status string `json:"status"` + Date time.Time `json:"date"` + KcalActive float64 `json:"kcal_active"` + KcalResting float64 `json:"kcal_resting"` + StartPoint StartPoint `json:"start_point"` + Distance float64 `json:"distance"` + Duration int `json:"duration"` + ElevationUp float64 `json:"elevation_up"` + ElevationDown float64 `json:"elevation_down"` + Sport string `json:"sport"` + MapImage MapImage `json:"map_image"` + Difficulty Difficulty `json:"difficulty"` + ChangedAt time.Time `json:"changed_at"` + Embedded DetailedTourEmbedded `json:"_embedded"` +} + +type Items struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + Alt float64 `json:"alt"` + T int `json:"t"` +} + +type Coordinates struct { + Items []Items `json:"items"` +} + +type DetailedTourEmbedded struct { + Coordinates Coordinates `json:"coordinates"` + Timeline Timeline `json:"timeline"` +} + +type Timeline struct { + Embedded TimelineEmbedded `json:"_embedded"` + Links Links `json:"_links"` + Page Page `json:"page"` +} + +type TimelineEmbedded struct { + Items []Item `json:"items"` +} + +type Item struct { + Index int `json:"index"` + Cover int `json:"cover"` + Type string `json:"type"` + Embedded TimelineItemEmbedded `json:"_embedded"` +} + +type TimelineItemEmbedded struct { + Reference Reference `json:"reference"` +} + +type Reference struct { + ID int `json:"id"` + Type string `json:"type"` + BaseName string `json:"base_name"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + ChangedAt time.Time `json:"changed_at"` + Sport string `json:"sport"` + Routable bool `json:"routable"` + StartPoint Point `json:"start_point"` + MidPoint Point `json:"mid_point"` + EndPoint Point `json:"end_point"` + Distance float64 `json:"distance"` + ElevationUp float64 `json:"elevation_up"` + ElevationDown float64 `json:"elevation_down"` + Score float64 `json:"score"` + WikiPOIID string `json:"wiki_poi_id"` + PoorQuality bool `json:"poor_quality"` + Categories []string `json:"categories"` + Flagged bool `json:"flagged"` + Links Links `json:"_links"` + Embedded SubEmbedded `json:"_embedded"` +} + +type Point struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + Alt float64 `json:"alt"` +} + +type Links struct { + Self Link `json:"self"` +} + +type Link struct { + Href string `json:"href"` + Templated bool `json:"templated,omitempty"` +} + +type SubEmbedded struct { + Creator Creator `json:"creator"` + Images Images `json:"images"` + Tips Tips `json:"tips"` +} + +type Creator struct { + Username string `json:"username"` + Avatar Avatar `json:"avatar"` + Status string `json:"status"` + Links Links `json:"_links"` + DisplayName string `json:"display_name"` + IsPremium bool `json:"is_premium"` +} + +type Avatar struct { + Src string `json:"src"` + Templated bool `json:"templated"` + Type string `json:"type"` +} + +type Images struct { + Embedded ImagesEmbedded `json:"_embedded"` + Links Links `json:"_links"` + Page Page `json:"page"` +} + +type ImagesEmbedded struct { + Items []ImageItem `json:"items"` +} + +type ImageItem struct { + ID int `json:"id"` + Src string `json:"src"` + Rating Rating `json:"rating"` + Templated bool `json:"templated"` + HighlightID int `json:"highlight_id"` + ClientHash string `json:"client_hash,omitempty"` + Type string `json:"type"` + Links Links `json:"_links"` + Embedded SubEmbedded `json:"_embedded"` +} + +type Rating struct { + Up int `json:"up"` + Down int `json:"down"` +} + +type Tips struct { + Embedded TipsEmbedded `json:"_embedded"` + Links Links `json:"_links"` + Page Page `json:"page"` +} + +type TipsEmbedded struct { + Items []TipItem `json:"items"` +} + +type TipItem struct { + ID int `json:"id"` + Text string `json:"text"` + Rating Rating `json:"rating"` + CreatedAt time.Time `json:"created_at"` + TextLanguage string `json:"text_language"` + TranslatedText string `json:"translated_text"` + TranslatedTextLanguage string `json:"translated_text_language"` + Attribution string `json:"attribution"` + HighlightID int `json:"highlight_id"` + Links Links `json:"_links"` + Embedded SubEmbedded `json:"_embedded"` +} diff --git a/db/cron/models.go b/db/integrations/strava/models.go similarity index 97% rename from db/cron/models.go rename to db/integrations/strava/models.go index 34667ba2..df13f3b8 100644 --- a/db/cron/models.go +++ b/db/integrations/strava/models.go @@ -1,4 +1,4 @@ -package cron +package strava import "time" @@ -11,17 +11,17 @@ type RefreshTokenRequest struct { type RefreshTokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` - ExpiresAt *int64 `json:"expires_at"` + ExpiresAt int64 `json:"expires_at"` } type StravaIntegration struct { - Active bool `json:"active"` - Routes bool `json:"routes"` - Activities bool `json:"activities"` - ClientID int32 `json:"clientId"` - ClientSecret string `json:"clientSecret"` - AccessToken *string `json:"accessToken,omitempty"` - RefreshToken *string `json:"refreshToken,omitempty"` - ExpiresAt *int64 `json:"expiresAt,omitempty"` + Active bool `json:"active"` + Routes bool `json:"routes"` + Activities bool `json:"activities"` + ClientID int32 `json:"clientId"` + ClientSecret string `json:"clientSecret"` + AccessToken string `json:"accessToken,omitempty"` + RefreshToken string `json:"refreshToken,omitempty"` + ExpiresAt int64 `json:"expiresAt,omitempty"` } type StravaRoute struct { Athlete Athlete `json:"athlete"` diff --git a/db/cron/strava.go b/db/integrations/strava/strava.go similarity index 92% rename from db/cron/strava.go rename to db/integrations/strava/strava.go index 066a1a5b..a5e17941 100644 --- a/db/cron/strava.go +++ b/db/integrations/strava/strava.go @@ -1,4 +1,4 @@ -package cron +package strava import ( "bytes" @@ -18,6 +18,10 @@ import ( "github.com/twpayne/go-polyline" ) +type StravaApi struct { + AceessToken string +} + func SyncStrava(app *pocketbase.PocketBase) error { integrations, err := app.Dao().FindRecordsByExpr("integrations", dbx.NewExp("true")) if err != nil { @@ -30,17 +34,26 @@ func SyncStrava(app *pocketbase.PocketBase) error { var stravaIntegration StravaIntegration json.Unmarshal([]byte(stravaString), &stravaIntegration) - if !stravaIntegration.Active || stravaIntegration.RefreshToken == nil { + if !stravaIntegration.Active || stravaIntegration.RefreshToken == "" { continue } - r, err := refreshStravaToken(stravaIntegration.ClientID, stravaIntegration.ClientSecret, *stravaIntegration.RefreshToken) + r, err := refreshStravaToken(stravaIntegration.ClientID, stravaIntegration.ClientSecret, stravaIntegration.RefreshToken) if err != nil { - return err + warning := fmt.Sprintf("error refreshing strava access token: %v\n", err) + fmt.Print(warning) + app.Logger().Warn(warning) + continue + } + if r.AccessToken != "" { + stravaIntegration.AccessToken = r.AccessToken + } + if r.RefreshToken != "" { + stravaIntegration.RefreshToken = r.RefreshToken + } + if r.AccessToken != "" { + stravaIntegration.ExpiresAt = r.ExpiresAt } - stravaIntegration.AccessToken = &r.AccessToken - stravaIntegration.RefreshToken = &r.RefreshToken - stravaIntegration.ExpiresAt = r.ExpiresAt b, err := json.Marshal(stravaIntegration) if err != nil { @@ -56,11 +69,17 @@ func SyncStrava(app *pocketbase.PocketBase) error { routes, err := fetchStravaRoutes(r.AccessToken, page) if err != nil { - return err + warning := fmt.Sprintf("error fetching routes from strava: %v\n", err) + fmt.Print(warning) + app.Logger().Warn(warning) + break } hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, routes) if err != nil { - return err + warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err) + fmt.Print(warning) + app.Logger().Warn(warning) + break } page += 1 } @@ -71,11 +90,17 @@ func SyncStrava(app *pocketbase.PocketBase) error { for hasNewActivities { activities, err := fetchStravaActivities(r.AccessToken, page) if err != nil { - return err + warning := fmt.Sprintf("error fetching activities from strava: %v", err) + fmt.Print(warning) + app.Logger().Warn(warning) + break } hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, activities) if err != nil { - return err + warning := fmt.Sprintf("error syncing strava activities with trails: %v", err) + fmt.Print(warning) + app.Logger().Warn(warning) + break } page += 1 } @@ -263,7 +288,7 @@ func createTrailFromRoute(app *pocketbase.PocketBase, route StravaRoute, gpx *fi lat = coords[0][0] lon = coords[0][1] } else { - fmt.Println("Warning: No coordinates available, setting lat/lon to 0") + app.Logger().Warn("Warning: No coordinates available, setting lat/lon to 0") lat, lon = 0, 0 } @@ -316,7 +341,7 @@ func createWaypointsFromRoute(app *pocketbase.PocketBase, route StravaRoute, use for i, wp := range route.Waypoints { record := models.NewRecord(collection) - record.Set("name", string(i)) + record.Set("name", strconv.Itoa(i)) record.Set("description", wp.Description) record.Set("lat", wp.Latlng[0]) record.Set("lon", wp.Latlng[1]) diff --git a/db/main.go b/db/main.go index 1297102c..3ab69c12 100644 --- a/db/main.go +++ b/db/main.go @@ -18,11 +18,12 @@ import ( "github.com/pocketbase/pocketbase/forms" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/plugins/migratecmd" - pbCron "github.com/pocketbase/pocketbase/tools/cron" + "github.com/pocketbase/pocketbase/tools/cron" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/hook" - "pocketbase/cron" + "pocketbase/integrations/komoot" + "pocketbase/integrations/strava" _ "pocketbase/migrations" "pocketbase/util" ) @@ -404,12 +405,20 @@ func registerRoutes(e *core.ServeEvent, app *pocketbase.PocketBase, client meili } func registerCronJobs(app *pocketbase.PocketBase) { - scheduler := pbCron.New() + scheduler := cron.New() - scheduler.MustAdd("strava", "*/15 * * * *", func() { - err := cron.SyncStrava(app) + scheduler.MustAdd("integrations", "0 * * * *", func() { + err := strava.SyncStrava(app) if err != nil { - app.Logger().Warn(fmt.Sprintf("Error syncing with strava: %v", err)) + warning := fmt.Sprintf("Error syncing with strava: %v", err) + fmt.Print(warning) + app.Logger().Error(warning) + } + err = komoot.SyncKomoot(app) + if err != nil { + warning := fmt.Sprintf("Error syncing with komoot: %v", err) + fmt.Print(warning) + app.Logger().Error(warning) } }) diff --git a/db/migrations/1738690714_updated_trails.go b/db/migrations/1738690714_updated_trails.go new file mode 100644 index 00000000..2ed2bde5 --- /dev/null +++ b/db/migrations/1738690714_updated_trails.go @@ -0,0 +1,75 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("e864strfxo14pm4") + if err != nil { + return err + } + + // update + edit_external_provider := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "htr35nha", + "name": "external_provider", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "strava", + "komoot" + ] + } + }`), edit_external_provider); err != nil { + return err + } + collection.Schema.AddField(edit_external_provider) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("e864strfxo14pm4") + if err != nil { + return err + } + + // update + edit_external_provider := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "htr35nha", + "name": "external_provider", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "strava" + ] + } + }`), edit_external_provider); err != nil { + return err + } + collection.Schema.AddField(edit_external_provider) + + return dao.SaveCollection(collection) + }) +} diff --git a/db/migrations/1738690729_updated_integrations.go b/db/migrations/1738690729_updated_integrations.go new file mode 100644 index 00000000..74358170 --- /dev/null +++ b/db/migrations/1738690729_updated_integrations.go @@ -0,0 +1,53 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("iz4sezoehde64wp") + if err != nil { + return err + } + + // add + new_komoot := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "6s0oxqgp", + "name": "komoot", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 2000000 + } + }`), new_komoot); err != nil { + return err + } + collection.Schema.AddField(new_komoot) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("iz4sezoehde64wp") + if err != nil { + return err + } + + // remove + collection.Schema.RemoveField("6s0oxqgp") + + return dao.SaveCollection(collection) + }) +} diff --git a/web/src/lib/components/profile/activity_card.svelte b/web/src/lib/components/profile/activity_card.svelte index 3e1550a8..d5deebed 100644 --- a/web/src/lib/components/profile/activity_card.svelte +++ b/web/src/lib/components/profile/activity_card.svelte @@ -72,7 +72,7 @@ {#if activity.photos.length}
+ {description} +
+- Syncs your strava routes with wanderer in regular intervals. -
-