Files
wanderer/plugins/strava/mapper.go
slothful-vassal 485ec53f6d feat: add plugin system (#1034)
* feat: add plugin system

* fix db docker build

* fix hammerhead readme, add strava subscription news to docs

* fixes and sdk improvements

* fix: reduce Meilisearch load, debounce federation sync (#1012)

* optimize meili trail index

* several fixes

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Bump svelte from 5.55.5 to 5.56.0 in /docs (#1032)

Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.55.5 to 5.56.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.56.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Release v0.19.2 (#1035)

* chore: release v0.19.2

* add changelog

---------

Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>

* speed up plugin sync and several small fixes

* concepts for security improvements and process stability

* improve concept

* security concept implemented

* remove insecure TLS

* worker concept implemented

* fixes and cleanup

* fixes

* docu

* mermaid, namings

* WASM plugin host improvements, plugin logging

* fix db migration

* Improve plugin config and category mapping UI

* fixes

* further fixes

* remove manual test sync

* fix db migration and strava mapping

* type added, UI improvements

* fix plugin card toggle clickable area

* optimize synch status card layout

* plugin type 'trails' instead of 'integration'

* session auth validation in UI

* fix komoot date and waypoints

* improve category mapping

* fix send to hammerhead: trail name

* plugin setup error handling improved

* fix review findings

* re-mapping added

* rename remote_category

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
2026-06-22 15:00:44 +02:00

229 lines
5.8 KiB
Go

//go:build tinygo
package main
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
sdkgpx "github.com/open-wanderer/wanderer/plugins/sdk/gpx"
)
func routeImport(route route, gpxData []byte) (trailImport, error) {
if len(gpxData) == 0 {
return trailImport{}, fmt.Errorf("route GPX is empty")
}
privacy := privacyFromPrivate(route.Private)
startedAt := time.Unix(route.Timestamp, 0).UTC().Format(time.RFC3339)
return trailImport{
Source: trailImportSource{
Provider: "strava",
ExternalID: route.IDStr,
},
Kind: "planned",
Name: route.Name,
Description: route.Description,
StartedAt: startedAt,
ActivityType: activityTypeForRoute(route.Type),
Privacy: &privacy,
Track: track{
Format: "gpx",
ContentBase64: base64.StdEncoding.EncodeToString(gpxData),
},
Waypoints: routeWaypoints(route),
Metadata: map[string]any{
"distance": route.Distance,
"elevationGain": route.ElevationGain,
"duration": route.EstimatedMovingTime,
"providerCategory": routeCategory(route.Type),
"estimatedMovingTime": route.EstimatedMovingTime,
},
}, nil
}
func activityImport(activity *detailedActivity, streams *activityStreamResponse, photos []activityPhoto) (trailImport, error) {
if len(activity.StartLatlng) < 2 {
return trailImport{}, fmt.Errorf("activity has no start coordinate")
}
gpxData, err := activityGPX(activity, streams)
if err != nil {
return trailImport{}, err
}
privacy := privacyFromPrivate(activity.Private)
return trailImport{
Source: trailImportSource{
Provider: "strava",
ExternalID: strconv.FormatInt(activity.ID, 10),
},
Kind: "completed",
Name: activity.Name,
Description: activity.Description,
StartedAt: activity.StartDate,
ActivityType: activityType(activity),
Privacy: &privacy,
Track: track{
Format: "gpx",
ContentBase64: base64.StdEncoding.EncodeToString(gpxData),
},
Photos: activityPhotos(activity, photos),
Metadata: map[string]any{
"distance": activity.Distance,
"elevationGain": activity.TotalElevationGain,
"duration": activity.ElapsedTime,
"providerCategory": providerActivityType(activity),
},
}, nil
}
func routeWaypoints(route route) []waypoint {
points := make([]waypoint, 0, len(route.Waypoints))
for i, wp := range route.Waypoints {
if len(wp.Latlng) < 2 {
continue
}
name := wp.Title
if name == "" {
name = strconv.Itoa(i)
}
points = append(points, waypoint{
Name: name,
Description: wp.Description,
Lat: wp.Latlng[0],
Lon: wp.Latlng[1],
Icon: "circle",
})
}
return points
}
func activityPhotos(activity *detailedActivity, apiPhotos []activityPhoto) []photo {
photos := make([]photo, 0, len(apiPhotos))
seen := make(map[string]bool, len(apiPhotos))
for _, apiPhoto := range apiPhotos {
url := apiPhoto.Urls.Num600
if url == "" {
url = apiPhoto.Urls.Num100
}
if url == "" {
continue
}
externalID := apiPhoto.UniqueID
if externalID == "" {
externalID = url
}
if seen[externalID] {
continue
}
seen[externalID] = true
photos = append(photos, photo{
ExternalID: externalID,
Filename: fmt.Sprintf("strava-%s.jpg", safePhotoID(externalID)),
Source: mediaSource{
Type: "url",
URL: url,
},
})
}
if len(photos) > 0 {
return photos
}
if activity.Photos.Primary.Urls.Num600 == "" {
return nil
}
externalID := strconv.FormatInt(activity.Photos.Primary.ID, 10)
return []photo{{
ExternalID: externalID,
Filename: fmt.Sprintf("strava-%s.jpg", externalID),
Source: mediaSource{
Type: "url",
URL: activity.Photos.Primary.Urls.Num600,
},
}}
}
func safePhotoID(value string) string {
replacer := strings.NewReplacer("/", "-", "\\", "-", ":", "-", "?", "-", "&", "-", "=", "-")
return replacer.Replace(value)
}
func activityGPX(activity *detailedActivity, streams *activityStreamResponse) ([]byte, error) {
if streams == nil || len(streams.LatLng.Data) == 0 {
return nil, fmt.Errorf("activity has no latlng stream")
}
startedAt, _ := time.Parse(time.RFC3339, activity.StartDate)
points := make([]sdkgpx.Point, 0, len(streams.LatLng.Data))
for i, latlng := range streams.LatLng.Data {
if len(latlng) < 2 || i >= len(streams.Time.Data) {
continue
}
elevation := 0.0
if i < len(streams.Altitude.Data) {
elevation = streams.Altitude.Data[i]
}
point := sdkgpx.Point{
Lat: latlng[0],
Lon: latlng[1],
Elevation: &elevation,
}
if !startedAt.IsZero() {
pointTime := startedAt.Add(time.Duration(streams.Time.Data[i]) * time.Second).UTC()
point.Time = &pointTime
}
points = append(points, point)
}
return sdkgpx.Track("wanderer Strava plugin", activity.Name, points)
}
func privacyFromPrivate(private bool) string {
if private {
return "private"
}
return "public"
}
func activityTypeForRoute(routeType int) string {
switch routeType {
case 1:
return "biking"
case 2:
return "walking"
default:
return ""
}
}
func routeCategory(routeType int) string {
return fmt.Sprintf("route:%d", routeType)
}
func providerActivityType(activity *detailedActivity) string {
if activity.SportType != "" {
return activity.SportType
}
return activity.Type
}
func activityType(activity *detailedActivity) string {
value := providerActivityType(activity)
switch value {
case "AlpineSki", "BackcountrySki", "IceSkate", "NordicSki", "RollerSki", "Snowboard":
return "skiing"
case "Canoeing", "Kayaking", "Kitesurf", "Rowing", "Sail", "StandUpPaddling", "Surfing", "Windsurf":
return "canoeing"
case "Hike", "Snowshoe":
return "hiking"
case "Run", "VirtualRun", "Walk", "Golf", "Skateboard", "Wheelchair":
return "walking"
case "Ride", "EBikeRide", "Handcycle", "InlineSkate", "Velomobile", "VirtualRide":
return "biking"
case "RockClimbing":
return "climbing"
default:
return value
}
}