* 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>
229 lines
5.8 KiB
Go
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
|
|
}
|
|
}
|