From 7d8c2bd7d3104fc562355dc81a6d8407ef3589d7 Mon Sep 17 00:00:00 2001 From: Vladimir Fetisov Date: Thu, 4 Apr 2024 17:38:24 +0200 Subject: [PATCH 01/10] Don't use process.env --- src/config.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++------ src/index.ts | 50 ++++++++++++++++++++++---------------------------- 2 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/config.ts b/src/config.ts index 56313ce..96317ce 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,22 +1,60 @@ +import { LLU_API_ENDPOINTS } from './constants/llu-api-endpoints'; + function readConfig() { - const requiredEnvs = ['NIGHTSCOUT_API_TOKEN', 'NIGHTSCOUT_URL']; + const requiredEnvs = [ + 'NIGHTSCOUT_API_TOKEN', + 'NIGHTSCOUT_URL', + 'LINK_UP_USERNAME', + 'LINK_UP_PASSWORD', + ]; for (let envName of requiredEnvs) { if (!process.env[envName]) { - throw Error(`Required environment variable ${envName} is not set`); + exitLog(`Required environment variable ${envName} is not set`) } } - const protocol = + if (process.env.LOG_LEVEL) { + if (!['info', 'debug'].includes(process.env.LOG_LEVEL.toLowerCase())) { + exitLog(`LOG_LEVEL should be either 'info' or 'debug', but got '${process.env.LOG_LEVEL}'`); + } + } + if (process.env.LINK_UP_REGION) { + if (!LLU_API_ENDPOINTS.hasOwnProperty(process.env.LINK_UP_REGION)) { + exitLog(`LINK_UP_REGION should be one of ${Object.keys(LLU_API_ENDPOINTS)}, but got ${process.env.LINK_UP_REGION}`); + } + } + if (process.env.LINK_UP_TIME_INTERVAL) { + if (isNaN(parseInt(process.env.LINK_UP_TIME_INTERVAL))) { + exitLog(`LINK_UP_TIME_INTERVAL expected to be an integer, but got '${process.env.LINK_UP_TIME_INTERVAL}'`); + } + } + + const protocol = process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true' ? 'http://' : 'https://'; const url = new URL(protocol + process.env.NIGHTSCOUT_URL); return { nightscoutApiToken: process.env.NIGHTSCOUT_API_TOKEN as string, - nightscoutBaseUrl: url.toString(), + nightscoutBaseUrl: url.toString(), + linkUpUsername: process.env.LINK_UP_USERNAME as string, + linkUpPassword: process.env.LINK_UP_PASSWORD as string, + + logLevel: process.env.LOG_LEVEL || 'info', + singleShot: process.env.SINGLE_SHOT === 'true', - nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true', - nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up', + nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true', + nightscoutDisableHttps: process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true', + nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up', + + linkUpRegion: process.env.LINK_UP_REGION || 'EU', + linkUpTimeInterval: Number(process.env.LINK_UP_TIME_INTERVAL) || 5, + linkUpConnection: process.env.LINK_UP_CONNECTION as string, }; } +function exitLog(msg: string) { + console.log(msg); + process.exit(1); +} + export default readConfig; diff --git a/src/index.ts b/src/index.ts index 253601d..85fba11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import {HttpCookieAgent} from "http-cookie-agent/http"; import {Agent as HttpAgent} from "node:http"; import {Agent as HttpsAgent} from "node:https"; import * as crypto from "crypto"; +import { isNativeError } from "node:util/types"; // Generate new Cyphers for stealth mode in order to bypass SSL fingerprinting used by Cloudflare. // The new Cyphers are then used in the HTTPS Agent for Axios. @@ -51,41 +52,34 @@ const logger = createLogger({ logFormat ), transports: [ - new transports.Console({level: process.env.LOG_LEVEL || "info"}), + new transports.Console({level: config.logLevel}), ] }); -axios.interceptors.response.use(response => -{ - return response; -}, error => -{ - if (error.response) +axios.interceptors.response.use( + response => response, + error => { - logger.error(JSON.stringify(error.response.data)); - } - else - { - logger.error(error.message); + if (error.response) + { + logger.error(JSON.stringify(error.response.data)); + } + else + { + logger.error(error.message); + } + return error; } - return error; -}); +); const USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1"; -/** - * LibreLink Up Credentials - */ -const LINK_UP_USERNAME = process.env.LINK_UP_USERNAME; -const LINK_UP_PASSWORD = process.env.LINK_UP_PASSWORD; - /** * LibreLink Up API Settings (Don't change this unless you know what you are doing) */ const LIBRE_LINK_UP_VERSION = "4.7.0"; const LIBRE_LINK_UP_PRODUCT = "llu.ios"; -const LINK_UP_REGION = process.env.LINK_UP_REGION || "EU"; -const LIBRE_LINK_UP_URL = getLibreLinkUpUrl(LINK_UP_REGION); +const LIBRE_LINK_UP_URL = getLibreLinkUpUrl(config.linkUpRegion); function getLibreLinkUpUrl(region: string): string { @@ -108,13 +102,13 @@ const libreLinkUpHttpHeaders: LibreLinkUpHttpHeaders = { "product": LIBRE_LINK_UP_PRODUCT } -if (process.env.SINGLE_SHOT === "true") +if (config.singleShot) { main().then(); } else { - const schedule = "*/" + (process.env.LINK_UP_TIME_INTERVAL || 5) + " * * * *"; + const schedule = `*/${config.linkUpTimeInterval} * * * *`; logger.info("Starting cron schedule: " + schedule) cron.schedule(schedule, () => { @@ -156,8 +150,8 @@ export async function login(): Promise const response: { data: LoginResponse } = await axios.post( url, { - email: LINK_UP_USERNAME, - password: LINK_UP_PASSWORD, + email: config.linkUpUsername, + password: config.linkUpPassword, }, { headers: libreLinkUpHttpHeaders, @@ -255,14 +249,14 @@ export async function getLibreLinkUpConnection(): Promise dumpConnectionData(connectionData); - if (!process.env.LINK_UP_CONNECTION) + if (!config.linkUpConnection) { logger.warn("You did not specify a Patient-ID in the LINK_UP_CONNECTION environment variable."); logPickedUpConnection(connectionData[0]); return connectionData[0].patientId; } - const connection = connectionData.filter(connectionEntry => connectionEntry.patientId === process.env.LINK_UP_CONNECTION)[0]; + const connection = connectionData.filter(connectionEntry => connectionEntry.patientId === config.linkUpConnection)[0]; if (!connection) { logger.error("The specified Patient-ID was not found."); From 545b993554d5164029d40e3a60ad2178059cb470 Mon Sep 17 00:00:00 2001 From: Vladimir Fetisov Date: Thu, 4 Apr 2024 18:02:57 +0200 Subject: [PATCH 02/10] Remove getLibreLinkUpUrl --- src/index.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index 85fba11..14a4057 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,6 @@ import {HttpCookieAgent} from "http-cookie-agent/http"; import {Agent as HttpAgent} from "node:http"; import {Agent as HttpsAgent} from "node:https"; import * as crypto from "crypto"; -import { isNativeError } from "node:util/types"; // Generate new Cyphers for stealth mode in order to bypass SSL fingerprinting used by Cloudflare. // The new Cyphers are then used in the HTTPS Agent for Axios. @@ -79,16 +78,7 @@ const USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) App */ const LIBRE_LINK_UP_VERSION = "4.7.0"; const LIBRE_LINK_UP_PRODUCT = "llu.ios"; -const LIBRE_LINK_UP_URL = getLibreLinkUpUrl(config.linkUpRegion); - -function getLibreLinkUpUrl(region: string): string -{ - if (LLU_API_ENDPOINTS.hasOwnProperty(region)) - { - return LLU_API_ENDPOINTS[region]; - } - return LLU_API_ENDPOINTS.EU; -} +const LIBRE_LINK_UP_URL = LLU_API_ENDPOINTS[config.linkUpRegion]; /** * last known authTicket @@ -140,7 +130,7 @@ async function main(): Promise } await uploadToNightScout(glucoseGraphData); -} + } export async function login(): Promise { @@ -311,7 +301,7 @@ export async function createFormattedMeasurements(measurementData: GraphData): P async function uploadToNightScout(measurementData: GraphData): Promise { const formattedMeasurements: Entry[] = await createFormattedMeasurements(measurementData); - + if (formattedMeasurements.length > 0) { logger.info("Trying to upload " + formattedMeasurements.length + " glucose measurement items to Nightscout"); From 5f867a9595eb5f84e807c5ba081ebc58280e8d5f Mon Sep 17 00:00:00 2001 From: Vladimir Fetisov Date: Thu, 4 Apr 2024 18:24:22 +0200 Subject: [PATCH 03/10] Fix ident --- src/config.ts | 52 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/config.ts b/src/config.ts index 96317ce..e88d9f6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,41 +14,41 @@ function readConfig() { } if (process.env.LOG_LEVEL) { - if (!['info', 'debug'].includes(process.env.LOG_LEVEL.toLowerCase())) { + if (!['info', 'debug'].includes(process.env.LOG_LEVEL.toLowerCase())) { exitLog(`LOG_LEVEL should be either 'info' or 'debug', but got '${process.env.LOG_LEVEL}'`); - } - } - if (process.env.LINK_UP_REGION) { - if (!LLU_API_ENDPOINTS.hasOwnProperty(process.env.LINK_UP_REGION)) { - exitLog(`LINK_UP_REGION should be one of ${Object.keys(LLU_API_ENDPOINTS)}, but got ${process.env.LINK_UP_REGION}`); - } - } - if (process.env.LINK_UP_TIME_INTERVAL) { - if (isNaN(parseInt(process.env.LINK_UP_TIME_INTERVAL))) { - exitLog(`LINK_UP_TIME_INTERVAL expected to be an integer, but got '${process.env.LINK_UP_TIME_INTERVAL}'`); - } - } - - const protocol = + } + } + if (process.env.LINK_UP_REGION) { + if (!LLU_API_ENDPOINTS.hasOwnProperty(process.env.LINK_UP_REGION)) { + exitLog(`LINK_UP_REGION should be one of ${Object.keys(LLU_API_ENDPOINTS)}, but got ${process.env.LINK_UP_REGION}`); + } + } + if (process.env.LINK_UP_TIME_INTERVAL) { + if (isNaN(parseInt(process.env.LINK_UP_TIME_INTERVAL))) { + exitLog(`LINK_UP_TIME_INTERVAL expected to be an integer, but got '${process.env.LINK_UP_TIME_INTERVAL}'`); + } + } + + const protocol = process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true' ? 'http://' : 'https://'; const url = new URL(protocol + process.env.NIGHTSCOUT_URL); return { nightscoutApiToken: process.env.NIGHTSCOUT_API_TOKEN as string, - nightscoutBaseUrl: url.toString(), - linkUpUsername: process.env.LINK_UP_USERNAME as string, - linkUpPassword: process.env.LINK_UP_PASSWORD as string, + nightscoutBaseUrl: url.toString(), + linkUpUsername: process.env.LINK_UP_USERNAME as string, + linkUpPassword: process.env.LINK_UP_PASSWORD as string, - logLevel: process.env.LOG_LEVEL || 'info', - singleShot: process.env.SINGLE_SHOT === 'true', + logLevel: process.env.LOG_LEVEL || 'info', + singleShot: process.env.SINGLE_SHOT === 'true', - nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true', - nightscoutDisableHttps: process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true', - nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up', + nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true', + nightscoutDisableHttps: process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true', + nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up', - linkUpRegion: process.env.LINK_UP_REGION || 'EU', - linkUpTimeInterval: Number(process.env.LINK_UP_TIME_INTERVAL) || 5, - linkUpConnection: process.env.LINK_UP_CONNECTION as string, + linkUpRegion: process.env.LINK_UP_REGION || 'EU', + linkUpTimeInterval: Number(process.env.LINK_UP_TIME_INTERVAL) || 5, + linkUpConnection: process.env.LINK_UP_CONNECTION as string, }; } From 6c5322066ecf6f935ebade43fa04a52e8e91a3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Schl=C3=BCter?= Date: Fri, 5 Apr 2024 18:11:14 +0200 Subject: [PATCH 04/10] Release 2.6.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5d5050..20fefff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nightscout-librelink-up", - "version": "2.5.1", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nightscout-librelink-up", - "version": "2.5.1", + "version": "2.6.0", "license": "MIT", "dependencies": { "axios": "~1.6.8", diff --git a/package.json b/package.json index b5306fe..3e3c10a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nightscout-librelink-up", - "version": "2.5.1", + "version": "2.6.0", "description": "Script written in TypeScript that uploads CGM readings from LibreLink Up to Nightscout", "main": "dist/index.js", "scripts": { From f5310f6e3f9a2446eaadd5993659c4ccdb4b5381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Schl=C3=BCter?= Date: Fri, 5 Apr 2024 18:28:13 +0200 Subject: [PATCH 05/10] Update LLU version and user agent, shuffle Ciphers --- src/index.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 253601d..1705388 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,12 +24,18 @@ import {Agent as HttpAgent} from "node:http"; import {Agent as HttpsAgent} from "node:https"; import * as crypto from "crypto"; -// Generate new Cyphers for stealth mode in order to bypass SSL fingerprinting used by Cloudflare. -// The new Cyphers are then used in the HTTPS Agent for Axios. -const defaultCyphers: Array = crypto.constants.defaultCipherList.split(":"); -const stealthCyphers: Array = defaultCyphers.slice(0, 3); +// Generate new Ciphers for stealth mode in order to bypass SSL fingerprinting used by Cloudflare. +// The new Ciphers are then used in the HTTPS Agent for Axios. +const defaultCiphers: Array = crypto.constants.defaultCipherList.split(":"); +const stealthCiphers: Array = [ + defaultCiphers[0], + defaultCiphers[2], + defaultCiphers[1], + ...defaultCiphers.slice(3) +]; + const stealthHttpsAgent: HttpsAgent = new HttpsAgent({ - ciphers: stealthCyphers.join(":") + ciphers: stealthCiphers.join(":") }); // Create a new CookieJar and HttpCookieAgent for Axios to handle cookies. @@ -71,7 +77,7 @@ axios.interceptors.response.use(response => return error; }); -const USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1"; +const USER_AGENT = "Mozilla/5.0 (iPhone; CPU OS 17_4.1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/17.4.1 Mobile/10A5355d Safari/8536.25"; /** * LibreLink Up Credentials @@ -82,7 +88,7 @@ const LINK_UP_PASSWORD = process.env.LINK_UP_PASSWORD; /** * LibreLink Up API Settings (Don't change this unless you know what you are doing) */ -const LIBRE_LINK_UP_VERSION = "4.7.0"; +const LIBRE_LINK_UP_VERSION = "4.10.0"; const LIBRE_LINK_UP_PRODUCT = "llu.ios"; const LINK_UP_REGION = process.env.LINK_UP_REGION || "EU"; const LIBRE_LINK_UP_URL = getLibreLinkUpUrl(LINK_UP_REGION); @@ -165,7 +171,7 @@ export async function login(): Promise httpAgent: cookieAgent, httpsAgent: stealthHttpsAgent }); - + try { if (response.data.status !== 0) From 43cc8e3a06c9ff6da6688a6221937d5570596940 Mon Sep 17 00:00:00 2001 From: Toni Corvera Date: Fri, 3 May 2024 12:11:59 +0200 Subject: [PATCH 06/10] Add .dockerignore. Removes all git-related stuff from the image. Reduces unnecessary rebuilds. --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e723d12 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git* +README.md From 60a6d9fd9f51ae7432465d5f33c2935d99e4af34 Mon Sep 17 00:00:00 2001 From: Toni Corvera Date: Fri, 3 May 2024 12:16:18 +0200 Subject: [PATCH 07/10] Switch Debian base image to slim variant --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a34f964..9002810 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-bookworm +FROM node:20-bookworm-slim LABEL description="Script written in TypeScript that uploads CGM readings from LibreLink Up to Nightscout" # Create app directory From 9e25aa874c6a3571ccff2c1368fde17979b4f01b Mon Sep 17 00:00:00 2001 From: Toni Corvera Date: Fri, 3 May 2024 12:20:45 +0200 Subject: [PATCH 08/10] Use a separate build stage --- Dockerfile | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9002810..32a3e2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -FROM node:20-bookworm-slim -LABEL description="Script written in TypeScript that uploads CGM readings from LibreLink Up to Nightscout" +FROM node:20-bookworm-slim AS build-stage # Create app directory RUN mkdir -p /usr/src/app @@ -13,9 +12,14 @@ RUN npm install COPY . /usr/src/app # Run tests -RUN npm run test +RUN npm run test ; \ + rm -r tests coverage -RUN rm -r tests -RUN rm -r coverage +FROM node:20-bookworm-slim +LABEL description="Script written in TypeScript that uploads CGM readings from LibreLink Up to Nightscout" + +COPY --from=build-stage /usr/src/app /usr/src/app + +WORKDIR /usr/src/app CMD [ "npm", "start" ] From 3b15a13c9b3b4815af60f318858e0b9ab3715aec Mon Sep 17 00:00:00 2001 From: Toni Corvera Date: Fri, 3 May 2024 12:43:30 +0200 Subject: [PATCH 09/10] Remove devDependencies. Side-effect: Compile TypeScript and use the start-heroku script to run compiled code --- Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 32a3e2b..04a225c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,12 @@ COPY . /usr/src/app RUN npm run test ; \ rm -r tests coverage +# Compile +RUN npm run build + +# Remove devel-only dependencies +RUN npm prune --omit dev + FROM node:20-bookworm-slim LABEL description="Script written in TypeScript that uploads CGM readings from LibreLink Up to Nightscout" @@ -22,4 +28,4 @@ COPY --from=build-stage /usr/src/app /usr/src/app WORKDIR /usr/src/app -CMD [ "npm", "start" ] +CMD [ "npm", "run", "start-heroku" ] From adbda12e1f162a1b6cbff895dd46c3fcd5157972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Schl=C3=BCter?= Date: Tue, 14 May 2024 19:08:36 +0200 Subject: [PATCH 10/10] Fix for unit tests --- src/config.ts | 137 +++++++++++-------- src/index.ts | 15 +- tests/unit-tests/librelink/librelink.test.ts | 13 +- 3 files changed, 98 insertions(+), 67 deletions(-) diff --git a/src/config.ts b/src/config.ts index e88d9f6..f4519bc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,60 +1,87 @@ -import { LLU_API_ENDPOINTS } from './constants/llu-api-endpoints'; - -function readConfig() { - const requiredEnvs = [ - 'NIGHTSCOUT_API_TOKEN', - 'NIGHTSCOUT_URL', - 'LINK_UP_USERNAME', - 'LINK_UP_PASSWORD', - ]; - for (let envName of requiredEnvs) { - if (!process.env[envName]) { - exitLog(`Required environment variable ${envName} is not set`) - } - } - - if (process.env.LOG_LEVEL) { - if (!['info', 'debug'].includes(process.env.LOG_LEVEL.toLowerCase())) { - exitLog(`LOG_LEVEL should be either 'info' or 'debug', but got '${process.env.LOG_LEVEL}'`); - } - } - if (process.env.LINK_UP_REGION) { - if (!LLU_API_ENDPOINTS.hasOwnProperty(process.env.LINK_UP_REGION)) { - exitLog(`LINK_UP_REGION should be one of ${Object.keys(LLU_API_ENDPOINTS)}, but got ${process.env.LINK_UP_REGION}`); - } - } - if (process.env.LINK_UP_TIME_INTERVAL) { - if (isNaN(parseInt(process.env.LINK_UP_TIME_INTERVAL))) { - exitLog(`LINK_UP_TIME_INTERVAL expected to be an integer, but got '${process.env.LINK_UP_TIME_INTERVAL}'`); - } - } - - const protocol = - process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true' ? 'http://' : 'https://'; - const url = new URL(protocol + process.env.NIGHTSCOUT_URL); - - return { - nightscoutApiToken: process.env.NIGHTSCOUT_API_TOKEN as string, - nightscoutBaseUrl: url.toString(), - linkUpUsername: process.env.LINK_UP_USERNAME as string, - linkUpPassword: process.env.LINK_UP_PASSWORD as string, - - logLevel: process.env.LOG_LEVEL || 'info', - singleShot: process.env.SINGLE_SHOT === 'true', - - nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true', - nightscoutDisableHttps: process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true', - nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up', - - linkUpRegion: process.env.LINK_UP_REGION || 'EU', - linkUpTimeInterval: Number(process.env.LINK_UP_TIME_INTERVAL) || 5, - linkUpConnection: process.env.LINK_UP_CONNECTION as string, - }; +import {LLU_API_ENDPOINTS} from './constants/llu-api-endpoints'; + +function readConfig() +{ + let requiredEnvs: string[] = []; + + if (!isTest()) + { + requiredEnvs = [ + 'NIGHTSCOUT_API_TOKEN', + 'NIGHTSCOUT_URL', + 'LINK_UP_USERNAME', + 'LINK_UP_PASSWORD', + ]; + } + + for (let envName of requiredEnvs) + { + if (!process.env[envName]) + { + exitLog(`Required environment variable ${envName} is not set`) + } + } + + if (process.env.LOG_LEVEL) + { + if (!['info', 'debug'].includes(process.env.LOG_LEVEL.toLowerCase())) + { + exitLog(`LOG_LEVEL should be either 'info' or 'debug', but got '${process.env.LOG_LEVEL}'`); + } + } + + if (process.env.LINK_UP_REGION) + { + if (!LLU_API_ENDPOINTS.hasOwnProperty(process.env.LINK_UP_REGION)) + { + exitLog(`LINK_UP_REGION should be one of ${Object.keys(LLU_API_ENDPOINTS)}, but got ${process.env.LINK_UP_REGION}`); + } + } + + if (process.env.LINK_UP_TIME_INTERVAL) + { + if (isNaN(parseInt(process.env.LINK_UP_TIME_INTERVAL))) + { + exitLog(`LINK_UP_TIME_INTERVAL expected to be an integer, but got '${process.env.LINK_UP_TIME_INTERVAL}'`); + } + } + + const protocol = + process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true' ? 'http://' : 'https://'; + const url = new URL(protocol + process.env.NIGHTSCOUT_URL); + + return { + nightscoutApiToken: process.env.NIGHTSCOUT_API_TOKEN as string, + nightscoutBaseUrl: url.toString(), + linkUpUsername: process.env.LINK_UP_USERNAME as string, + linkUpPassword: process.env.LINK_UP_PASSWORD as string, + + logLevel: process.env.LOG_LEVEL || 'info', + singleShot: process.env.SINGLE_SHOT === 'true', + + nightscoutApiV3: process.env.NIGHTSCOUT_API_V3 === 'true', + nightscoutDisableHttps: process.env.NIGHTSCOUT_DISABLE_HTTPS === 'true', + nightscoutDevice: process.env.DEVICE_NAME || 'nightscout-librelink-up', + + linkUpRegion: process.env.LINK_UP_REGION || 'EU', + linkUpTimeInterval: Number(process.env.LINK_UP_TIME_INTERVAL) || 5, + linkUpConnection: process.env.LINK_UP_CONNECTION as string, + }; +} + +function exitLog(msg: string): void +{ + console.log(msg); + + if (!isTest()) + { + process.exit(1); + } } -function exitLog(msg: string) { - console.log(msg); - process.exit(1); +function isTest(): boolean +{ + return typeof jest !== "undefined"; } export default readConfig; diff --git a/src/index.ts b/src/index.ts index 2e3ab92..6b81066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,7 @@ const stealthHttpsAgent: HttpsAgent = new HttpsAgent({ const jar: CookieJar = new CookieJar(); const cookieAgent: HttpAgent = new HttpCookieAgent({cookies: {jar}}) -const config = readConfig(); +let config = readConfig(); const {combine, timestamp, printf} = format; @@ -62,7 +62,7 @@ const logger = createLogger({ }); axios.interceptors.response.use( - response => response, + response => response, error => { if (error.response) @@ -136,10 +136,12 @@ async function main(): Promise } await uploadToNightScout(glucoseGraphData); - } +} export async function login(): Promise { + config = readConfig() + try { const url = "https://" + LIBRE_LINK_UP_URL + "/llu/auth/login" @@ -187,6 +189,8 @@ export async function login(): Promise export async function getGlucoseMeasurements(): Promise { + config = readConfig() + try { const connectionId = await getLibreLinkUpConnection(); @@ -216,6 +220,8 @@ export async function getGlucoseMeasurements(): Promise export async function getLibreLinkUpConnection(): Promise { + config = readConfig() + try { const url = "https://" + LIBRE_LINK_UP_URL + "/llu/connections" @@ -253,6 +259,7 @@ export async function getLibreLinkUpConnection(): Promise } const connection = connectionData.filter(connectionEntry => connectionEntry.patientId === config.linkUpConnection)[0]; + if (!connection) { logger.error("The specified Patient-ID was not found."); @@ -307,7 +314,7 @@ export async function createFormattedMeasurements(measurementData: GraphData): P async function uploadToNightScout(measurementData: GraphData): Promise { const formattedMeasurements: Entry[] = await createFormattedMeasurements(measurementData); - + if (formattedMeasurements.length > 0) { logger.info("Trying to upload " + formattedMeasurements.length + " glucose measurement items to Nightscout"); diff --git a/tests/unit-tests/librelink/librelink.test.ts b/tests/unit-tests/librelink/librelink.test.ts index 32ddb00..c0cc30a 100644 --- a/tests/unit-tests/librelink/librelink.test.ts +++ b/tests/unit-tests/librelink/librelink.test.ts @@ -1,14 +1,7 @@ import "jest"; -import { - createFormattedMeasurements, - getGlucoseMeasurements, - getLibreLinkUpConnection, - login, -} from "../../../src"; +import {createFormattedMeasurements, getGlucoseMeasurements, getLibreLinkUpConnection, login,} from "../../../src"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; - -const mock = new MockAdapter(axios); import {default as loginSuccessResponse} from "../../data/login.json"; import {default as loginFailedResponse} from "../../data/login-failed.json"; import {default as connectionsResponse} from "../../data/connections.json"; @@ -17,6 +10,9 @@ import {default as graphResponse} from "../../data/graph.json"; import {AuthTicket} from "../../../src/interfaces/librelink/common"; import {GraphData} from "../../../src/interfaces/librelink/graph-response"; import {Entry} from "../../../src/nightscout/interface"; +import readConfig from "../../../src/config"; + +const mock = new MockAdapter(axios); mock.onPost("https://api-eu.libreview.io/llu/auth/login").reply(200, loginSuccessResponse); mock.onGet("https://api-eu.libreview.io/llu/connections").reply(200, connectionsResponse); @@ -64,6 +60,7 @@ describe("LibreLink Up", () => it("Get available connections - Second available patient-id", async () => { process.env.LINK_UP_CONNECTION = "77179667-ba4b-11eb-ad1f-0242ac110004"; + const config = readConfig(); const connectionId: string | null = await getLibreLinkUpConnection(); expect(connectionId).toBe("77179667-ba4b-11eb-ad1f-0242ac110004"); });