@@ -1,6 +1,7 @@
|
||||
import gc from 'garmin-connect';
|
||||
import Trimlog from './lib/client.mjs';
|
||||
import { generateTrimlogInput, today } from './lib/tcx.mjs';
|
||||
import fs from "fs/promises";
|
||||
import { today } from './lib/tcx.mjs';
|
||||
|
||||
async function main() {
|
||||
const garmin = new gc.GarminConnect({
|
||||
@@ -12,14 +13,14 @@ async function main() {
|
||||
password: process.env.TRIMLOG_PASSWORD,
|
||||
})
|
||||
|
||||
await trimlog.refreshToken();
|
||||
await trimlog.submit({});
|
||||
await garmin.login();
|
||||
|
||||
console.log("CONNECTED!");
|
||||
|
||||
const blackList = {};
|
||||
|
||||
setInterval(async() => {
|
||||
const looseActivities = await trimlog.getLooseActivities();
|
||||
const recentWorkouts = await trimlog.getWorkouts();
|
||||
|
||||
const activities = await garmin.getActivities();
|
||||
const todayDate = today();
|
||||
@@ -40,25 +41,16 @@ async function main() {
|
||||
blackList[id] = true;
|
||||
continue;
|
||||
}
|
||||
if (looseActivities.find(l => l.tags.find(t => t.key === "gc:ActivityID" && t.value === id))) {
|
||||
console.error("Skipping", act.activityId, "(loose activity exists)");
|
||||
blackList[id] = true;
|
||||
continue;
|
||||
}
|
||||
if (recentWorkouts.find(l => l.tags.find(t => t.key === "gc:ActivityID" && t.value === id))) {
|
||||
console.error("Skipping", act.activityId, "(workout exists)");
|
||||
blackList[id] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log("Going ahead with", act.activityId, act.activityType.typeKey)
|
||||
|
||||
await garmin.downloadOriginalActivityData(act, "/tmp/", "tcx")
|
||||
const input = await generateTrimlogInput(`/tmp/${act.activityId}.tcx`, act.activityName, act.activityType?.typeKey)
|
||||
|
||||
input.tags.push({key: "gc:ActivityID", value: id})
|
||||
|
||||
await trimlog.postWorkout(input);
|
||||
const input = (await fs.readFile(`/tmp/${act.activityId}.tcx`)).toString("base64")
|
||||
|
||||
await trimlog.submit({addUpload: {
|
||||
name: `${act.activityId}.tcx`,
|
||||
base64: input,
|
||||
}});
|
||||
|
||||
blackList[id] = true;
|
||||
}
|
||||
|
||||
+156
-44
@@ -1,56 +1,168 @@
|
||||
import WebSocket from "ws";
|
||||
|
||||
export default class Trimlog {
|
||||
constructor({username, password}) {
|
||||
this.token = null;
|
||||
this.tokenExpiry = new Date(0);
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.socket = null;
|
||||
this.inbox = [];
|
||||
}
|
||||
|
||||
async getLooseActivities() {
|
||||
return this.fetch("GET", "activities");
|
||||
}
|
||||
|
||||
async getWorkouts() {
|
||||
return this.fetch("GET", `workouts?from=${new Date(Date.now() - 86400000 * 3).toISOString().slice(0, 10)}`);
|
||||
}
|
||||
|
||||
async postWorkout(data) {
|
||||
return this.fetch("POST", "workouts", data);
|
||||
}
|
||||
|
||||
async fetch(method, path, body) {
|
||||
if (new Date() > this.tokenExpiry) {
|
||||
await this.refreshToken();
|
||||
}
|
||||
|
||||
const res = await fetch("https://i.stifred.dev/api/"+path, {
|
||||
method: method,
|
||||
headers: {
|
||||
"content-type": body ? "application/json" : void(0),
|
||||
"authorization": `Bearer ${this.token}`
|
||||
send(key, data) {
|
||||
this.socket.send(JSON.stringify({
|
||||
[key]: data,
|
||||
register: {
|
||||
clientId: this.username,
|
||||
clientSecret: this.password,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : void(0),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
||||
return (await res.json()).data;
|
||||
}));
|
||||
}
|
||||
|
||||
async refreshToken() {
|
||||
const res = await fetch("https://stifred.auth.eu-west-1.amazoncognito.com/oauth2/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
"authorization": `Basic ${Buffer.from(`${this.username}:${this.password}`).toString("base64")}`
|
||||
},
|
||||
body: "grant_type=client_credentials&scope=indigo/api",
|
||||
})
|
||||
const data = await res.json();
|
||||
waitFor(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = this.socket;
|
||||
setInterval(() => {
|
||||
if (ws !== this.socket) {
|
||||
reject("disconnected");
|
||||
}
|
||||
|
||||
this.token = data.access_token;
|
||||
this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000 * 0.95));
|
||||
for (const [i, data] of this.inbox.entries()) {
|
||||
if (!!data[key]) {
|
||||
this.inbox.splice(i, 1);
|
||||
return data[key];
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.socket != null) {
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
let answered = false;
|
||||
const ws = new WebSocket("wss://i.stifred.dev/");
|
||||
|
||||
ws.on("open", () => {
|
||||
ws.send(JSON.stringify({register: {
|
||||
clientId: this.username,
|
||||
clientSecret: this.password,
|
||||
}}));
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (this.socket !== ws) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({ping: {
|
||||
reference: "ping:" + new Date().toISOString(),
|
||||
register: {
|
||||
clientId: this.username,
|
||||
clientSecret: this.password,
|
||||
},
|
||||
}}))
|
||||
}, 60000)
|
||||
|
||||
this.socket = ws;
|
||||
})
|
||||
|
||||
ws.on("message", (data) => {
|
||||
data = JSON.parse(data);
|
||||
if (data.signal) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(data, 0, 4));
|
||||
|
||||
if (!answered) {
|
||||
if (data.connection?.active) {
|
||||
answered = true;
|
||||
return resolve();
|
||||
} else if (data.error != null) {
|
||||
return reject(data.error.message);
|
||||
}
|
||||
}
|
||||
|
||||
this.inbox.push(data);
|
||||
})
|
||||
|
||||
ws.on("error", (err) => {
|
||||
if (!answered) {
|
||||
answered = true;
|
||||
return reject(err);
|
||||
}
|
||||
})
|
||||
|
||||
ws.on("close", () => {
|
||||
if (this.socket === ws) {
|
||||
this.socket = null;
|
||||
|
||||
console.log("Socket closed")
|
||||
this.connect().then(() => {
|
||||
console.log("Socket reconnected")
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
submit(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reference = Date.now().toString(36) + Math.random().toString(36).replace(".", "");
|
||||
|
||||
let answered = false;
|
||||
let sent = false;
|
||||
const ws = new WebSocket("wss://i.stifred.dev/");
|
||||
|
||||
ws.on("open", () => {
|
||||
ws.send(JSON.stringify({register: {
|
||||
clientId: this.username,
|
||||
clientSecret: this.password,
|
||||
}}));
|
||||
})
|
||||
|
||||
ws.on("message", (data) => {
|
||||
data = JSON.parse(data);
|
||||
console.log(JSON.stringify(data, 0, 4));
|
||||
|
||||
if (data.signal?.reference === reference) {
|
||||
if (!answered) {
|
||||
answered = true;
|
||||
resolve();
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!answered && !sent) {
|
||||
if (data.connection?.active) {
|
||||
ws.send(JSON.stringify({
|
||||
...message,
|
||||
ping: { reference },
|
||||
register: {
|
||||
clientId: this.username,
|
||||
clientSecret: this.password,
|
||||
}
|
||||
}));
|
||||
|
||||
sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.inbox.push(data);
|
||||
})
|
||||
|
||||
ws.on("error", (err) => {
|
||||
if (!answered) {
|
||||
answered = true;
|
||||
return reject(err);
|
||||
}
|
||||
})
|
||||
|
||||
ws.on("close", () => {
|
||||
console.log("Socket closed")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+27
@@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"dependencies": {
|
||||
"garmin-connect": "^1.6.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
},
|
||||
@@ -303,6 +304,26 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
@@ -535,6 +556,12 @@
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"garmin-connect": "^1.6.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import Trimlog from './lib/client.mjs';
|
||||
|
||||
async function readStdin() {
|
||||
return new Promise((resolve) => {
|
||||
const allLines = [];
|
||||
let lingeringLine = "";
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', function(chunk) {
|
||||
const lines = chunk.split("\n");
|
||||
|
||||
lines[0] = lingeringLine + lines[0];
|
||||
lingeringLine = lines.pop();
|
||||
|
||||
allLines.push(...lines);
|
||||
});
|
||||
|
||||
process.stdin.on('end', function() {
|
||||
allLines.push(lingeringLine);
|
||||
resolve(allLines);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function parseLine(line) {
|
||||
const split = line.split(" ");
|
||||
const split2 = split[0].split(/[:,.]/g);
|
||||
let seconds = parseInt(split2[0], 10) * 60;
|
||||
if (split2.length > 1) {
|
||||
seconds += parseInt(split2[1], 10);
|
||||
}
|
||||
|
||||
return ({
|
||||
seconds,
|
||||
calories: parseInt(split[1]),
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [id] = process.argv.slice(2);
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stuff = (await readStdin())
|
||||
.filter(l => l)
|
||||
.map(parseLine)
|
||||
.map(l => ({...l, roundId: id}))
|
||||
|
||||
console.log(stuff);
|
||||
|
||||
const trimlog = new Trimlog({
|
||||
username: process.env.TRIMLOG_USERNAME,
|
||||
password: process.env.TRIMLOG_PASSWORD,
|
||||
})
|
||||
|
||||
await trimlog.connect();
|
||||
console.log("CONNECTED!")
|
||||
|
||||
trimlog.send("addRoundMeasurementBatch", {
|
||||
measurements: stuff,
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
process.on('SIGINT', function() {
|
||||
console.log("INTERRUPTED");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function() {
|
||||
console.log("TERMINATED");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user