Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sugar Daddy Diabetes extension #15534

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SugarDaddyDiabetes Changelog

## [Initial Version] - 2024-03-XX
- Initial version of SugarDaddyDiabetes
- Real-time glucose monitoring through LibreView
- 24-hour glucose average with visual gauge
- Color-coded readings
- Menu bar quick view
- Automatic updates every 5 minutes
- Support for both mmol/L and mg/dL units
146 changes: 125 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,135 @@
<p align="center">
<img src="images/store-logo.webp" height="128">
<h1 align="center">Raycast Extensions</h1>
</p>
# SugarDaddyDiabetes

<p align="center">
<a aria-label="Follow Raycast on X" href="https://x.com/raycastapp">
<img alt="" src="https://img.shields.io/badge/Follow%[email protected]?style=for-the-badge&logo=X">
</a>
<a aria-label="Join the community on Slack" href="https://raycast.com/community">
<img alt="" src="https://img.shields.io/badge/Join%20the%20community-black.svg?style=for-the-badge&logo=Slack">
</a>
</p>
A Raycast extension that helps monitor glucose data from Freestyle Libre 2 & 3 devices through LibreView integration.

[Raycast](https://raycast.com/) lets you control your tools with a few keystrokes. This repository contains all extensions that are available in the [Raycast Store](https://raycast.com/store). It also includes documentation and examples of how to extend Raycast using React.
## Important Note About Data Timing

![Header](images/header.webp)
Please be aware that the glucose readings shown in this extension may be slightly delayed compared to your actual sensor readings. This delay occurs because:

## Getting Started
1. The data comes from LibreView's servers, not directly from your sensor
2. LibreView's API provides data that is typically 5-15 minutes behind real-time readings
3. The sync frequency depends on your sensor's connection to your phone and the phone's connection to LibreView

Visit [https://developers.raycast.com](https://developers.raycast.com) to get started with our API. If you want to discover and install extensions, check out [our Store](https://raycast.com/store).
For the most up-to-date readings, please refer to your Libre sensor directly or the LibreLink app on your phone.

Be sure to read and follow our [Community](https://manual.raycast.com/community-guidelines) and [Extension](https://manual.raycast.com/extensions) guidelines when submitting your extension and interacting with other folks in this repository.
## Features

## Feedback
- 📊 Near real-time glucose monitoring through LibreView (5-15 minute delay)
- 📈 24-hour glucose average with visual gauge
- 🎯 Color-coded readings (In Range 🟢, Low 🟡, High 🔴)
- 🔔 Menu bar quick view for latest readings
- 🔄 Automatic updates every 5 minutes
- 📱 Support for both mmol/L and mg/dL units

Raycast wouldn't be where it is without the feedback from our community, so we would be happy to hear what you think of the API / DevX and how we can improve. Please use [GitHub issues](https://github.com/raycast/extensions/issues/new/choose) for everything API related (bugs, improvements suggestions, developer experience, docs, etc). We have a few [templates](https://developers.raycast.com/examples) that should help you get started.
## Prerequisites

## Community
Before using this extension, you need:

Join our [Slack community](https://raycast.com/community) to share your extension, debug nasty bugs or simply get to know like-minded folks.
1. A LibreView account (https://www.libreview.com)
2. The LibreLinkUp mobile app installed and configured
3. A Freestyle Libre 2 or 3 sensor actively sharing data
4. Raycast installed on your Mac (https://raycast.com)

## Setup Instructions

1. Install the extension from the Raycast Store
2. Configure the required preferences:
- LibreView Username (your account email)
- LibreView Password
- Preferred Glucose Unit (mmol/L or mg/dL)
3. If you haven't set up LibreLinkUp sharing:
1. Download the LibreLinkUp app
2. Log in with your LibreView credentials
3. Add the account that has the Libre sensor
4. Wait for them to accept the invitation
5. Once connected, the extension will start showing data

## LibreView API Requirements

This extension uses the LibreView API with the following specifications:

- Authentication: Token-based with 50-minute expiry
- Rate Limiting: Implements automatic retry with 1-minute delay
- Data Refresh: Every 5 minutes for menu bar updates
- Data Latency: Readings may be 5-15 minutes behind real-time sensor data
- API Endpoints Used:
- Login: `/llu/auth/login`
- Connections: `/llu/connections`
- Glucose Data: `/llu/connections/{patientId}/graph`

## Privacy Policy

SugarDaddyDiabetes takes your privacy and data security seriously:

1. Data Collection
- The extension only collects necessary glucose data from LibreView
- No personal data is stored locally
- Credentials are securely stored in Raycast's preference system

2. Data Handling
- All data is fetched in real-time from LibreView
- No historical data is cached or stored
- Data is only displayed within the Raycast interface

3. Data Transmission
- All API communications use secure HTTPS
- Authentication tokens are stored temporarily in memory
- No data is shared with third parties

4. Security Measures
- Credentials are stored securely using Raycast's encryption
- API tokens expire after 50 minutes
- Rate limiting protection is implemented
- No sensitive data is logged or stored

## ⚠️ Important Medical Disclaimer

**THIS IS NOT A MEDICAL DEVICE AND SHOULD NOT BE USED AS ONE.**

This software is provided for informational purposes only and is not intended to be a substitute for professional medical advice, diagnosis, or treatment. Never rely on this software for making medical decisions. Always seek the advice of your physician or other qualified health provider regarding any medical condition and before starting, changing, or stopping any medical treatment.

Key points:
- This is an unofficial extension and is not affiliated with Abbott, LibreView, or any medical device manufacturer
- Do not make treatment decisions based on the information provided by this extension
- The data shown may be delayed, inaccurate, or unavailable
- Always confirm readings with your actual medical device
- This extension is not a replacement for proper diabetes management tools
- In case of emergency, contact your healthcare provider or emergency services

## Limitation of Liability

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

By using this extension, you acknowledge and agree that:
1. The developers are not medical professionals or experts
2. The extension may contain errors or inaccuracies
3. You use the extension at your own risk
4. The developers are not liable for any decisions made based on the information provided
5. The developers are not responsible for any harm that may result from using this extension

## Troubleshooting

If you encounter issues:

1. Verify your LibreView credentials are correct
2. Ensure your LibreLinkUp connection is active
3. Check if your sensor is actively sharing data
4. Try refreshing the extension
5. Ensure you have a stable internet connection

For additional support, please open an issue on GitHub.

## Support & Contact

For questions, feedback, and compliments, please contact:
- Email: [email protected]

## License

This project is licensed under the MIT License - see the LICENSE file for details.

## Acknowledgments

- This extension uses the LibreView API but is not endorsed by or affiliated with Abbott Laboratories or LibreView
- Freestyle Libre is a trademark of Abbott Laboratories
- All trademarks, service marks, trade names, product names and logos are the property of their respective owners
Binary file added assets/extension-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
181 changes: 181 additions & 0 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { clearLocalStorage, showToast, Toast, LocalStorage } from "@raycast/api";
import fetch from "node-fetch";
import { getLibreViewCredentials } from "./preferences";

const API_BASE = "https://api.libreview.io";
const API_HEADERS = {
"Content-Type": "application/json",
Product: "llu.android",
Version: "4.7.0",
"Accept-Encoding": "gzip",
};

const LOGGED_OUT_KEY = "logged_out";
const AUTH_TOKEN_KEY = "auth_token";

interface AuthResponse extends Record<string, unknown> {
status: number;
data: {
user: {
id: string;
firstName: string;
lastName: string;
email: string;
};
authTicket: {
token: string;
expires: number;
duration: number;
};
};
}

function isAuthResponse(data: unknown): data is AuthResponse {
if (typeof data !== "object" || data === null) return false;

const response = data as Record<string, unknown>;
return (
"status" in response &&
"data" in response &&
typeof response.data === "object" &&
response.data !== null &&
"authTicket" in (response.data as Record<string, unknown>) &&
typeof (response.data as Record<string, unknown>).authTicket === "object" &&
(response.data as Record<string, unknown>).authTicket !== null &&
"token" in ((response.data as Record<string, unknown>).authTicket as Record<string, unknown>) &&
typeof ((response.data as Record<string, unknown>).authTicket as Record<string, unknown>).token === "string"
);
}

interface ErrorResponse {
message?: string;
status?: number;
}

export async function authenticate(): Promise<string> {
console.log("Authenticating...");
const credentials = getLibreViewCredentials();

if (!credentials?.username || !credentials?.password) {
console.error("No credentials found");
throw new Error("Missing LibreView credentials");
}

try {
console.log("Sending auth request...");
const response = await fetch(`${API_BASE}/llu/auth/login`, {
method: "POST",
headers: {
...API_HEADERS,
Accept: "application/json",
},
body: JSON.stringify({
email: credentials.username,
password: credentials.password,
}),
});

const data = await response.json();
console.log("Auth response:", JSON.stringify(data, null, 2));

if (!response.ok) {
const data = (await response.json()) as ErrorResponse;
throw new Error(data.message || `Authentication failed: ${response.status}`);
}

if (!isAuthResponse(data)) {
console.error("Invalid response format:", data);
throw new Error("Invalid authentication response format");
}

const token = data.data.authTicket.token;
if (!token) {
throw new Error("No token in authentication response");
}

console.log("Authentication successful");
return token;
} catch (error) {
console.error("Authentication error:", error);

if (error instanceof Error) {
if (error.message.includes("401")) {
throw new Error("Invalid username or password. Please check your LibreView credentials.");
} else if (error.message.includes("429")) {
throw new Error("Too many login attempts. Please try again in a few minutes.");
} else if (error.message.includes("503")) {
throw new Error("LibreView service is temporarily unavailable. Please try again later.");
}
}
throw error;
}
}

export async function logout() {
try {
await clearLocalStorage();
await LocalStorage.removeItem(AUTH_TOKEN_KEY);
await LocalStorage.setItem(LOGGED_OUT_KEY, "true");
await showToast({
style: Toast.Style.Success,
title: "Logged out successfully",
message: "Please quit and restart the menu bar app",
});
} catch (error) {
console.error("Error during logout:", error);
await showToast({
style: Toast.Style.Failure,
title: "Failed to log out",
message: error instanceof Error ? error.message : "Unknown error",
});
}
}

type IsLoggedOutFunction = () => Promise<boolean>;

export const isLoggedOut: IsLoggedOutFunction = async () => {
try {
const value = await LocalStorage.getItem(LOGGED_OUT_KEY);
return value === "true";
} catch {
return false;
}
};

export async function clearLoggedOutState() {
await LocalStorage.removeItem(LOGGED_OUT_KEY);
}

export async function attemptLogin(): Promise<boolean> {
try {
const credentials = getLibreViewCredentials();
if (!credentials?.username || !credentials?.password) {
return false;
}

// Clear logged out state first
await clearLoggedOutState();

// Attempt authentication
const token = await authenticate();
if (token) {
await LocalStorage.setItem(AUTH_TOKEN_KEY, token);
await showToast({
style: Toast.Style.Success,
title: "Successfully logged in",
message: "Your LibreView credentials are valid",
});
return true;
}
return false;
} catch (error) {
console.error("Login attempt failed:", error);
await LocalStorage.setItem(LOGGED_OUT_KEY, "true");
await showToast({
style: Toast.Style.Failure,
title: "Login failed",
message: error instanceof Error ? error.message : "Unknown error",
});
return false;
}
}
Loading