diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..90988a9 --- /dev/null +++ b/config/config.go @@ -0,0 +1,7 @@ +package config + +// Config for application setup +type Config interface { + BadgerPath() (string, error) + PushoverAPIToken() (string, error) +} diff --git a/config/env.go b/config/env.go new file mode 100644 index 0000000..a1a2ed6 --- /dev/null +++ b/config/env.go @@ -0,0 +1,51 @@ +package config + +import ( + "errors" + "fmt" + "os" +) + +const ( + // BadgerPathEnv name + BadgerPathEnv = "BADGER_PATH" + // PushoverAPITokenEnv name + PushoverAPITokenEnv = "PUSHOVER_API_TOKEN" +) + +var ( + // ErrEnvVariableNotSet occurs when an environment variable is not set + ErrEnvVariableNotSet = errors.New("environment variable is not set") +) + +// Env variable Config implementation +type Env struct { +} + +// BadgerPath for the database directory +func (e *Env) BadgerPath() (string, error) { + val, ok := os.LookupEnv(BadgerPathEnv) + if !ok { + return "", fmt.Errorf( + "unable to get badger path from env variable %s: %w", + BadgerPathEnv, + ErrEnvVariableNotSet, + ) + } + + return val, nil +} + +// PushoverAPIToken getter +func (e *Env) PushoverAPIToken() (string, error) { + val, ok := os.LookupEnv(PushoverAPITokenEnv) + if !ok { + return "", fmt.Errorf( + "unable to get pushover API token from env variable %s: %w", + PushoverAPITokenEnv, + ErrEnvVariableNotSet, + ) + } + + return val, nil +} diff --git a/db/badger.go b/db/badger.go new file mode 100644 index 0000000..d30f987 --- /dev/null +++ b/db/badger.go @@ -0,0 +1,184 @@ +package db + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/dgraph-io/badger" +) + +// Badger db implementation +type Badger struct { + db *badger.DB + cancelGC func() + wg sync.WaitGroup +} + +// NewBadger creates a new badger instance for the given path +func NewBadger(dbPath string) (*Badger, error) { + db, err := badger.Open(badger.DefaultOptions(dbPath).WithLogger(nil)) + if err != nil { + return nil, fmt.Errorf("failed to open badger db at path %s: %w", dbPath, err) + } + + ctx, cancel := context.WithCancel(context.Background()) + + b := &Badger{ + db: db, + cancelGC: cancel, + } + + b.wg.Add(1) + go func() { + defer b.wg.Done() + + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + for b.db.RunValueLogGC(0.5) == nil && ctx.Err() == nil { + } + + case <-ctx.Done(): + return + } + } + }() + + return b, nil +} + +// Close the database +func (b *Badger) Close() error { + b.cancelGC() + b.wg.Wait() + + return b.db.Close() +} + +// AddUser to the database +func (b *Badger) AddUser(user *User) error { + return b.db.Update(func(tx *badger.Txn) error { + data, err := json.Marshal(user) + if err != nil { + return fmt.Errorf("failed to JSON marshal user: %w", err) + } + + key := user.badgerKey() + if _, err = tx.Get(key); err == nil { + return fmt.Errorf("user %s already exists", user.Name) + } + + return tx.Set(key, data) + }) +} + +// GetUser from the database +func (b *Badger) GetUser(username string) (user *User, err error) { + err = b.db.View(func(tx *badger.Txn) error { + item, err := tx.Get(badgerKeyForUsername(username)) + if err != nil { + return fmt.Errorf("failed to get user value for username %s: %w", username, err) + } + + user = &User{} + + return item.Value(func(val []byte) error { + err = json.Unmarshal(val, user) + if err != nil { + return fmt.Errorf("failed to unmarshal user value for username %s: %w", username, err) + } + + return nil + }) + }) + + return +} + +// ListUsers from the database +func (b *Badger) ListUsers() (users []*User, err error) { + err = b.db.View(func(tx *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.Prefix = []byte("user:") + + it := tx.NewIterator(opts) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + + err := item.Value(func(val []byte) error { + user := &User{} + err := json.Unmarshal(val, user) + if err != nil { + return fmt.Errorf("failed to unmarshal user value for user key %s: %w", string(item.Key()), err) + } + + users = append(users, user) + + return nil + }) + + if err != nil { + return err + } + } + + return nil + }) + + return +} + +// AddMedication to the database +func (b *Badger) AddMedication(medication *Medication) error { + return b.db.Update(func(tx *badger.Txn) error { + data, err := json.Marshal(medication) + if err != nil { + return fmt.Errorf("failed to JSON marshal medication: %w", err) + } + + return tx.Set(medication.badgerKey(), data) + }) +} + +// ListMedicationsForUser from the database +func (b *Badger) ListMedicationsForUser(user *User) (medications []*Medication, err error) { + err = b.db.View(func(tx *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.Prefix = badgerPrefixKeyForMedicationUser(user) + + it := tx.NewIterator(opts) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + + err := item.Value(func(val []byte) error { + medication := &Medication{} + err := json.Unmarshal(val, medication) + if err != nil { + return fmt.Errorf("failed to unmarshal medication value for medication key %s: %w", string(item.Key()), err) + } + + medications = append(medications, medication) + + return nil + }) + + if err != nil { + return err + } + } + + return nil + }) + + return +} diff --git a/db/medication.go b/db/medication.go new file mode 100644 index 0000000..336e203 --- /dev/null +++ b/db/medication.go @@ -0,0 +1,26 @@ +package db + +import ( + "time" + + "github.com/google/uuid" +) + +// Medication information for a user +type Medication struct { + IDUser uuid.UUID `json:"id_user"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + IntervalCrontab string `json:"interval_crontab"` + IntervalQuantity uint `json:"interval_quantity"` + IntervalPushoverDevices []string `json:"interval_pushover_devices"` + CreatedAt time.Time `json:"created_at"` +} + +func (m *Medication) badgerKey() []byte { + return append(append([]byte("medication:"), m.IDUser[:]...), m.ID[:]...) +} + +func badgerPrefixKeyForMedicationUser(user *User) []byte { + return append([]byte("medication:"), user.ID[:]...) +} diff --git a/db/user.go b/db/user.go new file mode 100644 index 0000000..df394f3 --- /dev/null +++ b/db/user.go @@ -0,0 +1,23 @@ +package db + +import ( + "time" + + "github.com/google/uuid" +) + +// User information +type User struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + PushoverDeviceTokens map[string]string `json:"pushover_device_tokens"` + CreatedAt time.Time `json:"created_at"` +} + +func (u *User) badgerKey() []byte { + return badgerKeyForUsername(u.Name) +} + +func badgerKeyForUsername(username string) []byte { + return append([]byte("user:"), []byte(username)...) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9657e35 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.0xdad.com/tblyler/meditime + +go 1.16 + +require ( + github.com/dgraph-io/badger v1.6.2 + github.com/dgraph-io/ristretto v0.0.4-0.20210122082011-bb5d392ed82d // indirect + github.com/google/uuid v1.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1622223 --- /dev/null +++ b/go.sum @@ -0,0 +1,110 @@ +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.4-0.20210122082011-bb5d392ed82d h1:eQYOG6A4td1tht0NdJB9Ls6DsXRGb2Ft6X9REU/MbbE= +github.com/dgraph-io/ristretto v0.0.4-0.20210122082011-bb5d392ed82d/go.mod h1:tv2ec8nA7vRpSYX7/MbP52ihrUMXIHit54CQMq8npXQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mmcloughlin/avo v0.0.0-20201105074841-5d2f697d268f/go.mod h1:6aKT4zZIrpGqB3RpFU14ByCSSyKY6LfJz4J/JJChHfI= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/twitchyliquid64/golang-asm v0.15.0/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= +golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..84c01ca --- /dev/null +++ b/main.go @@ -0,0 +1,257 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os" + "strconv" + "time" + + "git.0xdad.com/tblyler/meditime/config" + "git.0xdad.com/tblyler/meditime/db" + "github.com/google/uuid" +) + +func errLog(messages ...interface{}) { + fmt.Fprintln(os.Stderr, messages...) +} + +func log(messages ...interface{}) { + fmt.Println(messages...) +} + +func help() { +} + +func run(b *db.Badger) error { + users, err := b.ListUsers() + if err != nil { + return err + } + + for _, user := range users { + medications, err := b.ListMedicationsForUser(user) + if err != nil { + return err + } + + for _, medication := range medications { + // FIXME do the stuff + fmt.Println(user) + fmt.Println(medication) + } + } + + return nil +} + +func main() { + lenArgs := len(os.Args) + if lenArgs <= 1 { + help() + errLog("must supply at least one argument") + os.Exit(1) + } + + err := func() error { + inputScanner := bufio.NewScanner(os.Stdin) + config := config.Env{} + + badgerPath, err := config.BadgerPath() + if err != nil { + return err + } + + b, err := db.NewBadger(badgerPath) + if err != nil { + return err + } + + defer b.Close() + + switch os.Args[1] { + case "run": + return run(b) + + case "user": + if lenArgs < 3 { + return errors.New("must supply an argument to the user command") + } + + switch os.Args[2] { + case "add": + fmt.Print("username: ") + inputScanner.Scan() + + username := string(bytes.TrimSpace(inputScanner.Bytes())) + if username == "" { + return fmt.Errorf("failed to get username from STDIN prompt: %w", inputScanner.Err()) + } + + fmt.Print("pushover device token: ") + inputScanner.Scan() + + deviceToken := string(bytes.TrimSpace(inputScanner.Bytes())) + if deviceToken == "" { + return fmt.Errorf("no pushover device token provided: %w", inputScanner.Err()) + } + + id := uuid.New() + + err = b.AddUser(&db.User{ + ID: id, + Name: username, + PushoverDeviceTokens: map[string]string{ + "default": deviceToken, + }, + CreatedAt: time.Now(), + }) + if err != nil { + return fmt.Errorf("failed to insert username %s: %w", username, err) + } + + log("created user id", id) + + case "get": + fmt.Print("username: ") + inputScanner.Scan() + + username := string(bytes.TrimSpace(inputScanner.Bytes())) + if username == "" { + return fmt.Errorf("failed to get username from STDIN prompt: %w", inputScanner.Err()) + } + + user, err := b.GetUser(username) + if err != nil { + return err + } + + if user == nil { + return fmt.Errorf("username %s does not exist", username) + } + + log(user) + + case "list": + users, err := b.ListUsers() + if err != nil { + return err + } + + for _, user := range users { + fmt.Println(user) + } + } + + case "medication": + if lenArgs < 3 { + return errors.New("must supply an argument to the user command") + } + + switch os.Args[2] { + case "add": + fmt.Print("username: ") + inputScanner.Scan() + + username := string(bytes.TrimSpace(inputScanner.Bytes())) + if username == "" { + return fmt.Errorf("failed to get username from STDIN prompt: %w", inputScanner.Err()) + } + + user, err := b.GetUser(username) + if err != nil { + return fmt.Errorf("failed to lookup username %s: %w", username, err) + } + + if user == nil { + return fmt.Errorf("username %s doesn't exist", username) + } + + fmt.Print("name: ") + inputScanner.Scan() + + name := string(bytes.TrimSpace(inputScanner.Bytes())) + if name == "" { + return fmt.Errorf("failed to get medication name from STDIN prompt: %w", inputScanner.Err()) + } + + fmt.Print("cron schedule: ") + inputScanner.Scan() + + crontab := string(bytes.TrimSpace(inputScanner.Bytes())) + if crontab == "" { + return fmt.Errorf("failed to get cron schedule from STDIN prompt: %w", inputScanner.Err()) + } + + fmt.Print("interval quantity: ") + inputScanner.Scan() + + intervalQuantity, err := strconv.ParseUint(string(bytes.TrimSpace(inputScanner.Bytes())), 10, 64) + if intervalQuantity == 0 || err != nil { + return fmt.Errorf("failed to get interval quantity from STDIN prompt: %w", inputScanner.Err()) + } + + fmt.Print("interval pushover device token id: ") + inputScanner.Scan() + + intervalPushoverDevice := string(bytes.TrimSpace(inputScanner.Bytes())) + if _, ok := user.PushoverDeviceTokens[intervalPushoverDevice]; !ok { + return fmt.Errorf("the '%s' pushover device token doesn't exist for user %s", intervalPushoverDevice, user.Name) + } + + medication := &db.Medication{ + IDUser: user.ID, + ID: uuid.New(), + Name: name, + IntervalCrontab: crontab, + IntervalQuantity: uint(intervalQuantity), + IntervalPushoverDevices: []string{intervalPushoverDevice}, + CreatedAt: time.Now(), + } + + err = b.AddMedication(medication) + if err != nil { + return err + } + + log(medication) + + case "list": + fmt.Print("username: ") + inputScanner.Scan() + + username := string(bytes.TrimSpace(inputScanner.Bytes())) + if username == "" { + return fmt.Errorf("failed to get username from STDIN prompt: %w", inputScanner.Err()) + } + + user, err := b.GetUser(username) + if err != nil { + return fmt.Errorf("failed to lookup username %s: %w", username, err) + } + + if user == nil { + return fmt.Errorf("username %s doesn't exist", username) + } + + medications, err := b.ListMedicationsForUser(user) + if err != nil { + return err + } + + for _, medication := range medications { + log(medication) + } + } + } + + return nil + }() + + if err != nil { + errLog(err.Error()) + os.Exit(1) + } +}