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"` }