Gisle Aune
3 months ago
5 changed files with 265 additions and 56 deletions
-
28index.mjs
-
174lib/client.mjs
-
27package-lock.json
-
1package.json
-
77stufftxt.mjs
@ -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"); |
|||
send(key, data) { |
|||
this.socket.send(JSON.stringify({ |
|||
[key]: data, |
|||
register: { |
|||
clientId: this.username, |
|||
clientSecret: this.password, |
|||
}, |
|||
})); |
|||
} |
|||
|
|||
async getWorkouts() { |
|||
return this.fetch("GET", `workouts?from=${new Date(Date.now() - 86400000 * 3).toISOString().slice(0, 10)}`); |
|||
waitFor(key) { |
|||
return new Promise((resolve, reject) => { |
|||
const ws = this.socket; |
|||
setInterval(() => { |
|||
if (ws !== this.socket) { |
|||
reject("disconnected"); |
|||
} |
|||
|
|||
async postWorkout(data) { |
|||
return this.fetch("POST", "workouts", data); |
|||
for (const [i, data] of this.inbox.entries()) { |
|||
if (!!data[key]) { |
|||
this.inbox.splice(i, 1); |
|||
return data[key]; |
|||
} |
|||
} |
|||
}, 100) |
|||
}) |
|||
} |
|||
|
|||
async fetch(method, path, body) { |
|||
if (new Date() > this.tokenExpiry) { |
|||
await this.refreshToken(); |
|||
connect() { |
|||
return new Promise((resolve, reject) => { |
|||
if (this.socket != null) { |
|||
this.socket.close(); |
|||
} |
|||
|
|||
const res = await fetch("https://i.stifred.dev/api/"+path, { |
|||
method: method, |
|||
headers: { |
|||
"content-type": body ? "application/json" : void(0), |
|||
"authorization": `Bearer ${this.token}` |
|||
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, |
|||
}, |
|||
body: body ? JSON.stringify(body) : void(0), |
|||
}})) |
|||
}, 60000) |
|||
|
|||
this.socket = ws; |
|||
}) |
|||
|
|||
if (!res.ok) { |
|||
throw new Error(res.statusText); |
|||
ws.on("message", (data) => { |
|||
data = JSON.parse(data); |
|||
if (data.signal) { |
|||
return |
|||
} |
|||
|
|||
return (await res.json()).data; |
|||
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); |
|||
} |
|||
} |
|||
|
|||
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", |
|||
this.inbox.push(data); |
|||
}) |
|||
|
|||
ws.on("error", (err) => { |
|||
if (!answered) { |
|||
answered = true; |
|||
return reject(err); |
|||
} |
|||
}) |
|||
const data = await res.json(); |
|||
|
|||
this.token = data.access_token; |
|||
this.tokenExpiry = new Date(Date.now() + (data.expires_in * 1000 * 0.95)); |
|||
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") |
|||
}) |
|||
}) |
|||
} |
|||
} |
@ -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(); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue