2025-08-20 22:42:21 -07:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2025-08-20 22:51:25 -07:00
|
|
|
"log"
|
2025-08-20 22:42:21 -07:00
|
|
|
"net/http"
|
2026-03-26 20:31:26 -07:00
|
|
|
"net/url"
|
|
|
|
|
"strings"
|
2025-08-20 22:42:21 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type garminClient struct {
|
2026-03-26 20:31:26 -07:00
|
|
|
c *http.Client
|
|
|
|
|
mapshareID string
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
type garminResponse struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Error *struct {
|
|
|
|
|
Code int `json:"code"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
} `json:"error"`
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
func newGarminClient(mapshareID string) *garminClient {
|
|
|
|
|
return &garminClient{
|
|
|
|
|
c: &http.Client{},
|
|
|
|
|
mapshareID: mapshareID,
|
|
|
|
|
}
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
func (gc *garminClient) sendMessage(deviceIDs []string, sender, msg string) error {
|
|
|
|
|
for _, deviceID := range deviceIDs {
|
|
|
|
|
err := gc.sendToDevice(deviceID, sender, msg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("device %s: %w", deviceID, err)
|
|
|
|
|
}
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
2026-03-26 20:31:26 -07:00
|
|
|
return nil
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
func (gc *garminClient) sendToDevice(deviceID, sender, msg string) error {
|
|
|
|
|
data := url.Values{}
|
|
|
|
|
data.Set("deviceIds", deviceID)
|
|
|
|
|
data.Set("messageText", msg)
|
|
|
|
|
data.Set("fromAddr", sender)
|
2025-08-20 22:42:21 -07:00
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
endpoint := fmt.Sprintf("https://share.garmin.com/%s/Map/SendMessageToDevices", gc.mapshareID)
|
2025-08-20 22:42:21 -07:00
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
log.Printf("[->garmin] %s %s", endpoint, data.Encode())
|
2025-08-20 22:51:25 -07:00
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
req, err := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
|
2025-08-20 22:42:21 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:56:54 -07:00
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
|
|
|
|
req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
|
|
|
|
|
req.Header.Set("Origin", "https://share.garmin.com")
|
|
|
|
|
req.Header.Set("Referer", fmt.Sprintf("https://share.garmin.com/%s", gc.mapshareID))
|
|
|
|
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
|
|
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15")
|
2025-08-20 22:42:21 -07:00
|
|
|
|
|
|
|
|
resp, err := gc.c.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
log.Printf("[<-garmin] %d %s", resp.StatusCode, string(body))
|
|
|
|
|
|
2025-08-20 22:42:21 -07:00
|
|
|
if resp.StatusCode != 200 {
|
2026-03-26 20:31:26 -07:00
|
|
|
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
var grResp garminResponse
|
2025-08-20 23:00:10 -07:00
|
|
|
err = json.Unmarshal(body, &grResp)
|
2025-08-20 22:42:21 -07:00
|
|
|
if err != nil {
|
2026-03-26 20:31:26 -07:00
|
|
|
return fmt.Errorf("parse response: %w", err)
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:31:26 -07:00
|
|
|
if !grResp.Success {
|
|
|
|
|
if grResp.Error != nil {
|
|
|
|
|
return fmt.Errorf("garmin error %d: %s", grResp.Error.Code, grResp.Error.Msg)
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("garmin returned success=false")
|
2025-08-20 22:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|