Add Garmin LiveTrack session tracking
A Gmail-watching Apps Script (documented in docs/deployment.adoc) posts the session ID from each LiveTrack email to a new webhook, which stores it in data/livetrack.json; a new sidebar panel lists what's been received.
This commit is contained in:
1 parent
60170efda2
commit
f9ddf860ea
11 files changed
+279
No files matched your search
@@ -107,6 +107,73 @@ id/secret to store as the two `OAUTH_CLIENT_*` secrets above.
|
||||
usernames, e.g. `randy,friend1,friend2` — not their emails, the
|
||||
`login` field from Gitea's `/api/v1/user`.
|
||||
|
||||
== Garmin LiveTrack session webhook
|
||||
|
||||
`POST /api/livetrack/session` (`session_id`, `secret` form params) records a
|
||||
Garmin LiveTrack session ID to `data/livetrack.json`; `GET
|
||||
/api/livetrack/sessions` lists them, newest first. Intended caller: a Google
|
||||
Apps Script bound to the Gmail account that receives LiveTrack emails,
|
||||
triggered on a timer, extracting the session ID from the email and posting
|
||||
it here — the endpoint itself doesn't know anything about email or Garmin's
|
||||
API, just the ID string.
|
||||
|
||||
`secret` must match the `LIVETRACK_WEBHOOK_SECRET` repo secret (added to
|
||||
`.env` the same way as the others above). Unlike the other secrets, this one
|
||||
is optional — leaving it unset simply disables the endpoint (`halt 403` on
|
||||
every call) rather than failing the deploy, since LiveTrack tracking isn't
|
||||
core to the app.
|
||||
|
||||
The caller side is a Google Apps Script bound to the Gmail account that
|
||||
receives the LiveTrack emails (script.google.com, "New project"), run on a
|
||||
time-driven trigger:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
const WEBHOOK_URL = "https://map.priesol.net/api/livetrack/session";
|
||||
const WEBHOOK_SECRET = "PASTE_SAME_VALUE_AS_LIVETRACK_WEBHOOK_SECRET_IN_.env";
|
||||
const PROCESSED_LABEL = "livetrack-processed";
|
||||
|
||||
function checkLiveTrackEmail() {
|
||||
const label = GmailApp.getUserLabelByName(PROCESSED_LABEL) || GmailApp.createLabel(PROCESSED_LABEL);
|
||||
const threads = GmailApp.search(`from:noreply@garmin.com subject:LiveTrack -label:${PROCESSED_LABEL}`);
|
||||
|
||||
threads.forEach(thread => {
|
||||
// getBody() (raw HTML), not getPlainBody(): the email has no text/plain
|
||||
// part, only text/html, and the link only exists as an href attribute
|
||||
// — the auto-generated plain-text conversion isn't reliable for it.
|
||||
const body = thread.getMessages()[0].getBody();
|
||||
const match = body.match(/https:\/\/livetrack\.garmin\.com\/session\/([\w-]+)/);
|
||||
|
||||
if (match) {
|
||||
UrlFetchApp.fetch(WEBHOOK_URL, {
|
||||
method: "post",
|
||||
payload: { session_id: match[1], secret: WEBHOOK_SECRET }
|
||||
});
|
||||
}
|
||||
|
||||
thread.addLabel(label); // stops it being reprocessed next run
|
||||
});
|
||||
}
|
||||
----
|
||||
|
||||
The Gmail label (rather than marking the message read/unread) tracks which
|
||||
threads are already handled without touching the user's own inbox state.
|
||||
`subject:LiveTrack` matches regardless of the account's language — Garmin
|
||||
keeps "LiveTrack" as-is in the subject even in a localized
|
||||
("Sledujte LiveTrack používateľa ...") one.
|
||||
|
||||
Set the trigger from the editor: clock icon on the left (Triggers) → *Add
|
||||
Trigger* → function `checkLiveTrackEmail`, time-driven, "Minutes timer",
|
||||
every 5 minutes.
|
||||
|
||||
Verified against a real "started an activity" LiveTrack email
|
||||
(2026-09-06): sender `noreply@garmin.com`, link
|
||||
`https://livetrack.garmin.com/session/<uuid>/token/<token>` — the regex
|
||||
above only captures the `<uuid>` part, stopping at the next `/`. Garmin
|
||||
also sends other LiveTrack notifications (e.g. activity ended) that
|
||||
weren't checked — if one turns out to use a different link shape, the
|
||||
webhook simply won't receive that session ID; nothing breaks.
|
||||
|
||||
== CI deploy job
|
||||
|
||||
`.gitea/workflows/ci.yml`'s `deploy` job runs after `check` passes, only
|
||||
|
||||
Reference in new issue
Block a user