package golibre
import (
"context"
"net/http"
)
type AccountService struct {
client *client
}
func (a *AccountService) GetAccountDetails(ctx context.Context) (AccountDetailData, error) {
endpoint := "/account"
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
endpoint,
http.NoBody,
)
if err != nil {
return AccountDetailData{}, err
}
target := AccountDetailsResponse{}
if err := a.client.Do(req, &target); err != nil {
return AccountDetailData{}, err
}
return target.Data, nil
}
type AccountDetailsResponse BaseResponse[AccountDetailData]
type AccountDetailData struct {
User UserAccountData `json:"user"`
}
type UserAccountData struct {
ID UserID `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
DateOfBirth uint `json:"dateOfBirth"`
Email string `json:"email"`
Country string `json:"country"`
UILanguage string `json:"uiLanguage"`
CommunicationLanguage string `json:"communicationLanguage"`
AccountType string `json:"accountType"`
UOM string `json:"uom"`
DateFormat string `json:"dateFormat"`
TimeFormat string `json:"timeFormat"`
EmailDay []uint `json:"emailDay"`
System System `json:"system"`
Details map[string]any `json:"details"`
TwoFactor TwoFactor `json:"twoFactor"`
Created uint `json:"created"`
LastLogin uint `json:"lastLogin"`
}
package golibre
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
)
type client struct {
httpClient *http.Client
userAgent string
apiURL string
requestPreProcessors []RequestPreProcessor
authentication Authentication
jwt jwtAuth
}
type jwtAuth struct {
rawToken string
mutex *sync.RWMutex
}
func (c *client) do(request *http.Request, target any) error {
if request.URL.Host == "" {
request.URL.Host = c.apiURL
}
request.URL.Scheme = "https"
// Required
request.Header.Set("cache-control", "no-cache")
request.Header.Set("content-type", "application/json")
request.Header.Set("product", "llu.android")
request.Header.Set("version", "4.8.0")
for _, requestPreProcessor := range c.requestPreProcessors {
if err := requestPreProcessor.ProcessRequest(request); err != nil {
return err
}
}
response, err := c.httpClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("network request error: %d", response.StatusCode)
}
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return err
}
// NOTE: Libreview API returns HTTP Status OK for every request, and changes the "status" field
// in the response body based on the success / failure of a request.
responseErr := APIError{
RawResponse: response,
}
if err := json.Unmarshal(bodyBytes, &responseErr); err != nil {
return err
}
switch responseErr.Status {
case StatusOK:
if target != nil {
if err := json.Unmarshal(bodyBytes, target); err != nil {
return err
}
}
return nil
case StatusUnauthenticated:
c.jwt.mutex.Lock()
defer c.jwt.mutex.Unlock()
c.jwt.rawToken = ""
return &responseErr
default:
// Unknown status code, return the response error if possible
return &responseErr
}
}
func (c *client) Do(request *http.Request, target any) error {
if err := c.addAuthentication(request); err != nil {
return err
}
return c.do(request, target)
}
func (c *client) addAuthentication(r *http.Request) error {
if token, tokenExists := c.checkForAuthToken(); tokenExists {
r.Header.Set("Authorization", "Bearer "+token)
return nil
}
if err := c.getNewAuthToken(r.Context()); err != nil {
return err
}
return c.addAuthentication(r)
}
func (c *client) checkForAuthToken() (authToken string, authTokenExists bool) {
c.jwt.mutex.RLock()
defer c.jwt.mutex.RUnlock()
authToken = c.jwt.rawToken
authTokenExists = authToken != ""
return authToken, authTokenExists
}
func (c *client) getNewAuthToken(ctx context.Context) error {
c.jwt.mutex.Lock()
defer c.jwt.mutex.Unlock()
// NOTE: Assuming >1 thread requesting refresh, ensure that after first thread unlocks,
// subsequent threads check the JWT Token before requesting a new token
if c.jwt.rawToken != "" {
return nil
}
authenticationRequestBody, err := json.Marshal(c.authentication)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("https://%s/llu/auth/login", c.apiURL),
bytes.NewReader(authenticationRequestBody),
)
if err != nil {
return err
}
// Required
req.Header.Set("cache-control", "no-cache")
req.Header.Set("content-type", "application/json")
req.Header.Set("product", "llu.android")
req.Header.Set("version", "4.8.0")
for _, requestPreProcessor := range c.requestPreProcessors {
if err := requestPreProcessor.ProcessRequest(req); err != nil {
return err
}
}
response, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("network request error: %d", response.StatusCode)
}
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return err
}
target := LoginResponse{}
responseErr := APIError{
RawResponse: response,
}
if err := json.Unmarshal(bodyBytes, &responseErr); err != nil {
return err
}
switch responseErr.Status {
case StatusOK:
if err := json.Unmarshal(bodyBytes, &target); err != nil {
return err
}
c.jwt.rawToken = target.Data.AuthTicket.Token
return nil
case StatusUnauthenticated:
return &responseErr
default:
// Unknown status code, return the response error if possible
return &responseErr
}
}
type Authentication struct {
Email string `json:"email"`
Password string `json:"password"`
}
type BaseResponse[data []ConnectionData | ConnectionGraphData | UserData | AccountDetailData] struct {
Status uint `json:"status"`
Data data `json:"data"`
Ticket AuthTicket `json:"ticket"`
}
package golibre
import (
"fmt"
"log/slog"
"net/http"
)
type configOption func(s *config)
type config struct {
transport *http.Transport
existingJWTToken string
requestPreProcessors []RequestPreProcessor
}
type RequestPreProcessor interface {
ProcessRequest(r *http.Request) error
}
type RequestPreProcessorFunc func(*http.Request) error
func (p RequestPreProcessorFunc) ProcessRequest(r *http.Request) error {
return p(r)
}
func WithExistingJWTToken(existingToken string) configOption {
return func(s *config) {
s.existingJWTToken = existingToken
}
}
func WithTLSInsecureSkipVerify() configOption {
return func(s *config) {
s.transport.TLSClientConfig.InsecureSkipVerify = true
}
}
func WithRequestPreProcessor(requestPreProcessor RequestPreProcessor) configOption {
return func(s *config) {
s.requestPreProcessors = append(s.requestPreProcessors, requestPreProcessor)
}
}
func WithSlogger(logger *slog.Logger) configOption {
return WithRequestPreProcessor(&sloggerWrapper{
logger: logger,
})
}
type sloggerWrapper struct {
logger *slog.Logger
}
func (s *sloggerWrapper) ProcessRequest(r *http.Request) error {
s.logger.Debug(fmt.Sprintf("Request: %s %s", r.Method, r.URL.String()))
return nil
}
package golibre
import (
"context"
"net/http"
)
type ConnectionService struct {
client *client
}
func (c *ConnectionService) GetAllConnectionData(ctx context.Context) ([]ConnectionData, error) {
endpoint := "/llu/connections"
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
endpoint,
http.NoBody,
)
if err != nil {
return nil, err
}
target := GetAllConnectionsResponse{}
if err := c.client.Do(req, &target); err != nil {
return nil, err
}
return target.Data, nil
}
func (c *ConnectionService) GetConnectionGraph(ctx context.Context, patientID PatientID) (ConnectionGraphData, error) {
endpoint := "/llu/connections/" + string(patientID) + "/graph"
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
endpoint,
http.NoBody,
)
if err != nil {
return ConnectionGraphData{}, err
}
target := ConnectionGraphResponse{}
if err := c.client.Do(req, &target); err != nil {
return ConnectionGraphData{}, err
}
return target.Data, nil
}
type ConnectionGraphResponse BaseResponse[ConnectionGraphData]
type GetAllConnectionsResponse BaseResponse[[]ConnectionData]
type ConnectionGraphData struct {
Connection ConnectionData `json:"connection"`
ActiveSensors []SensorDevicePair `json:"activeSensors"`
GraphData []GraphGlucoseMeasurement `json:"graphData"`
}
type SensorDevicePair struct {
Sensor Sensor `json:"sensor"`
Device PatientDevice `json:"device"`
}
type GraphGlucoseMeasurement struct {
FactoryTimestamp Timestamp `json:"FactoryTimestamp"` //nolint:tagliatelle
Timestamp Timestamp `json:"Timestamp"` //nolint:tagliatelle
Type uint `json:"type"`
ValueInMgPerDl uint `json:"ValueInMgPerDl"` //nolint:tagliatelle
MeasurementColor uint `json:"MeasurementColor"` //nolint:tagliatelle
GlucoseUnits uint `json:"GlucoseUnits"` //nolint:tagliatelle
Value float32 `json:"Value"` //nolint:tagliatelle
IsHigh bool `json:"isHigh"`
IsLow bool `json:"isLow"`
}
type ConnectionData struct {
ID UserID `json:"id"`
PatientID PatientID `json:"patientId"`
Country string `json:"country"`
Status uint `json:"status"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
TargetLow uint `json:"targetLow"`
TargetHigh uint `json:"targetHigh"`
UnitOfMeasurement UnitOfMeasurement `json:"uom"`
Sensor Sensor `json:"sensor"`
AlarmRules AlarmRules `json:"alarmRules"`
GlucoseMeasurement GlucoseMeasurement `json:"glucoseMeasurement"`
GlucoseItem GlucoseMeasurement `json:"glucoseItem"`
GlucoseAlarm any `json:"glucoseAlarm"`
PatientDevice PatientDevice `json:"patientDevice"`
Created uint `json:"created"`
}
type PatientDevice struct {
DID string `json:"did"`
DTID uint `json:"dtid"`
V string `json:"v"`
LL uint `json:"ll"`
HL uint `json:"hl"`
U uint `json:"u"`
FixedLowAlarmValues FixedLowAlarmValues `json:"fixedLowAlarmValues"`
Alarms bool `json:"alarms"`
FixedLowThreshold uint `json:"fixedLowThreshold"`
}
type FixedLowAlarmValues struct {
MGDL uint `json:"mgdl"`
MMOLL float32 `json:"mmoll"`
}
type GlucoseMeasurement struct {
FactoryTimestamp Timestamp `json:"FactoryTimestamp"` //nolint:tagliatelle
Timestamp Timestamp `json:"Timestamp"` //nolint:tagliatelle
Type uint `json:"type"`
ValueInMgPerDl uint `json:"ValueInMgPerDl"` //nolint:tagliatelle
TrendArrow uint `json:"TrendArrow"` //nolint:tagliatelle
MeasurementColor uint `json:"MeasurementColor"` //nolint:tagliatelle
GlucoseUnits uint `json:"GlucoseUnits"` //nolint:tagliatelle
Value float32 `json:"Value"` //nolint:tagliatelle
IsHigh bool `json:"isHigh"`
IsLow bool `json:"isLow"`
}
type Sensor struct {
DeviceID string `json:"deviceId"`
SerialNumber string `json:"sn"`
Activated uint `json:"a"`
W uint `json:"w"`
PT uint `json:"pt"`
S bool `json:"s"`
LJ bool `json:"lj"`
}
type AlarmRules struct {
C bool `json:"c"`
H AlarmRuleH `json:"h"`
F AlarmRule `json:"f"`
L AlarmRule `json:"l"`
ND AlarmRuleND `json:"nd"`
P uint `json:"p"`
R uint `json:"r"`
STD any `json:"std"`
}
type AlarmRuleH struct {
TargetHigh uint `json:"th"`
TargetHighMMoL float32 `json:"thmm"`
D uint `json:"d"`
F float32 `json:"f"`
}
type AlarmRule struct {
TargetHigh uint `json:"th"`
TargetHighMMoL float32 `json:"thmm"`
D uint `json:"d"`
TargetLow uint `json:"tl"`
TargetLowMMoL float32 `json:"tlmm"`
}
type AlarmRuleND struct {
I uint `json:"i"`
R uint `json:"r"`
L uint `json:"l"`
}
package golibre
import (
"fmt"
"net/http"
)
type APIError struct {
RawResponse *http.Response `json:"rawResponse"`
Status StatusCode `json:"status"`
Detail ErrorMessage `json:"error"`
}
type ErrorMessage struct {
Message string `json:"message"`
}
func (e *APIError) Error() string {
if e.Detail.Message == "" {
return fmt.Sprintf("generic LibreView API error, network status code: %d", e.RawResponse.StatusCode)
}
return e.Detail.Message
}
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/equalsgibson/golibre"
)
func main() {
// Set up a new golibre service
ctx := context.Background()
service := golibre.NewService(
"api.libreview.io",
golibre.Authentication{
Email: os.Getenv("EMAIL"), // Your email address
Password: os.Getenv("PASSWORD"), // Your password
},
)
connections, err := service.Connection().GetAllConnectionData(ctx)
if err != nil {
log.Fatal(err)
}
// Print a count of all the patients that you are connected to, with a list of patient IDs
fmt.Printf("You have %d patients that are sharing their data with you.\n", len(connections))
for i, connection := range connections {
fmt.Printf("\t-> Patient %d: ID: %s\n", i+1, connection.PatientID)
}
}
package golibre
import (
"context"
"crypto/tls"
"net"
"net/http"
"sync"
"time"
)
const LibreViewAPIURL string = "api.libreview.io"
type Service struct {
client *client
accountService *AccountService
connectionService *ConnectionService
userService *UserService
}
func NewService(
apiURL string,
auth Authentication,
opts ...configOption,
) *Service {
config := &config{
transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext(ctx, network, addr)
},
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
},
},
}
for _, opt := range opts {
opt(config)
}
c := &client{
httpClient: &http.Client{
Transport: config.transport,
Timeout: 15 * time.Second,
},
authentication: auth,
apiURL: apiURL,
jwt: jwtAuth{
rawToken: config.existingJWTToken,
mutex: &sync.RWMutex{},
},
}
return &Service{
client: c,
accountService: &AccountService{
client: c,
},
connectionService: &ConnectionService{
client: c,
},
userService: &UserService{
client: c,
},
}
}
func (s *Service) Connection() *ConnectionService {
return s.connectionService
}
func (s *Service) User() *UserService {
return s.userService
}
func (s *Service) Account() *AccountService {
return s.accountService
}
package golibre
import (
"encoding/json"
"time"
)
type LoginResponse struct {
Status uint `json:"status"`
Data LoginData `json:"data"`
}
type LoginData struct {
User map[string]any `json:"user"`
Messages map[string]any `json:"messages"`
Notifications map[string]any `json:"notifications"`
AuthTicket AuthTicket `json:"authTicket"`
Invitations []any `json:"invitations"`
TrustedDeviceToken string `json:"trustedDeviceToken"`
}
type AuthTicket struct {
Token string `json:"token"`
Expires uint64 `json:"expires"`
Duration uint64 `json:"duration"`
}
type (
PatientID string
UserID string
UnitOfMeasurement uint
)
const (
MMOL UnitOfMeasurement = iota
DL
)
/*
Timestamp requires custom unmarshalling due to being returned
by the Libreview API in the following format: 'M/D/YYYY H:M:S PM'
Example data returned: "9/20/2024 3:42:05 PM".
*/
type Timestamp time.Time
func (t *Timestamp) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
time, err := time.Parse("1/2/2006 3:4:5 PM", s)
if err != nil {
return err
}
*t = Timestamp(time)
return nil
}
type StatusCode uint
const (
StatusOK StatusCode = iota
_
StatusUnauthenticated
)
package golibre
import (
"context"
"net/http"
)
type UserService struct {
client *client
}
func (u *UserService) GetLoggedInUser(ctx context.Context) (UserData, error) {
endpoint := "/user"
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
endpoint,
http.NoBody,
)
if err != nil {
return UserData{}, err
}
target := GetLoggedInUserResponse{}
if err := u.client.Do(req, &target); err != nil {
return UserData{}, err
}
return target.Data, nil
}
type GetLoggedInUserResponse BaseResponse[UserData]
type UserData struct {
User User `json:"user"`
Messages Messages `json:"messages"`
Notifications Notifications `json:"notifications"`
AuthTicket AuthTicket `json:"authTicket"`
Invitations []string `json:"invitations"`
TrustedDeviceToken string `json:"trustedDeviceToken"`
}
type User struct {
ID UserID `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
Country string `json:"country"`
UILanguage string `json:"uiLanguage"`
CommunicationLanguage string `json:"communicationLanguage"`
AccountType string `json:"accountType"`
UOM string `json:"uom"`
DateFormat string `json:"dateFormat"`
TimeFormat string `json:"timeFormat"`
EmailDay []uint `json:"emailDay"`
System System `json:"system"`
Details map[string]any `json:"details"`
TwoFactor TwoFactor `json:"twoFactor"`
Created uint `json:"created"`
LastLogin uint `json:"lastLogin"`
Programs map[string]any `json:"programs"`
DateOfBirth uint `json:"dateOfBirth"`
Devices map[string]any `json:"devices"`
Consents Consents `json:"consents"`
}
type Messages struct {
Unread uint `json:"unread"`
}
type Notifications struct {
Unresolved uint `json:"unresolved"`
}
type System struct {
Messages SystemMessages `json:"messages"`
}
type SystemMessages struct {
AppReviewBanner uint `json:"appReviewBanner"`
FirstUsePhoenix uint `json:"firstUsePhoenix"`
FirstUsePhoenixReportsDataMerged uint `json:"firstUsePhoenixReportsDataMerged"`
LLUGettingStartedBanner uint `json:"lluGettingStartedBanner"`
LLUNewFeatureModal uint `json:"lluNewFeatureModal"`
LLUOnboarding uint `json:"lluOnboarding"`
LVWebPostRelease string `json:"lvWebPostRelease"`
}
type TwoFactor struct {
PrimaryMethod string `json:"primaryMethod"`
PrimaryValue string `json:"primaryValue"`
SecondaryMethod string `json:"secondaryMethod"`
SecondaryValue string `json:"secondaryValue"`
}
type Consents struct {
LLU LLUConsent `json:"llu"`
RealWorldEvidence RealWorldEvidenceConsent `json:"realWorldEvidence"`
}
type LLUConsent struct {
PolicyAccept uint `json:"policyAccept"`
TOUAccept uint `json:"touAccept"`
}
type RealWorldEvidenceConsent struct {
PolicyAccept uint `json:"policyAccept"`
TOUAccept uint `json:"touAccept"`
History []PolicyAccept `json:"history"`
}
type PolicyAccept struct {
PolicyAccept uint `json:"policyAccept"`
}