diff --git a/Dockerfile b/Dockerfile index ccfff9b..a4fdccc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,38 @@ # Copyright (c) karl-cardenas-coding # SPDX-License-Identifier: Apache-2.0 -FROM golang:1.22.4-alpine3.20 as builder +FROM golang:1.22.5-alpine3.20 as builder LABEL org.opencontainers.image.source="https://github.com/karl-cardenas-coding/mywhoop" LABEL org.opencontainers.image.description "A tool for gathering and retaining your own Whoop data." ARG VERSION +RUN apk update && \ + apk add --no-cache ca-certificates tzdata xdg-utils && \ + update-ca-certificates && \ + adduser -H -u 1002 -D appuser appuser + ADD ./ /source -RUN apk update && apk add --no-cache ca-certificates tzdata && update-ca-certificates && \ -cd /source && \ -adduser -H -u 1002 -D appuser appuser && \ -go build -ldflags="-X 'github.com/karl-cardenas-coding/mywhoop/cmd.VersionString=${VERSION}'" -o whoop -v +WORKDIR /source +RUN go build -ldflags="-X 'github.com/karl-cardenas-coding/mywhoop/cmd.VersionString=${VERSION}'" -o mywhoop -v && \ +mkdir -p /app && chown -R appuser:appuser /app FROM scratch -COPY --from=builder /source/whoop /go/bin/whoop +USER appuser:appuser + +COPY --from=builder /source/mywhoop /bin/mywhoop +COPY --from=builder /usr/bin/xdg-open /usr/bin/xdg-open COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /etc/group /etc/group +COPY --from=builder /app /app - -USER appuser:appuser +EXPOSE 8080 -ENTRYPOINT ["/go/bin/whoop"] +ENTRYPOINT ["/bin/mywhoop"] diff --git a/README.md b/README.md index 6e5d778..6145c56 100644 --- a/README.md +++ b/README.md @@ -9,45 +9,103 @@ A tool for gathering and retaining your own Whoop data.
+## Overview + +MyWhoop is a tool intended to help you take ownership of your Whoop data. You can use MyWhoop to interface with your own data in ways that are different from what Whoop may offer or intend. MyWhoop is designed to be simple and easy to use and designed to be deployed on your own machine or server. MyWhoop supports the following features: + +- 🔐 **Login**: A simple interface to log into the Whoop developer portal and save an authentication token locally. The token is required to interact with the Whoop API. +- 🗄️ **Server**: Automatically download your Whoop data daily and save it to a local file or export it to a remote location. +- 📬 **Notifications**: Receive notifications when new data is available or when an error occurs. +- 💾 **Data Export**: Export your Whoop data to a remote location such as an S3 bucket. + +## Get Started 🚀 + +Please check out the [Getting Started](/docs/get-started.md) guide to get started with MyWhoop. + + +## Commands + +MyWhoop supports the following commands and global flags: +- [Dump](#dump) - Download your Whoop data and save it to a local file. +- [Login](#login) - Authenticate with the Whoop API and save the authentication token locally. +- [Help](#help) - Display help information for MyWhoop. +- [Server](#server) - Automatically download your Whoop data daily and save it to a local file or export it to a remote location. +- [Version](#version) - Display the version of MyWhoop. -## Get Started +#### Global Flags +| Long Flag | Short Flag |Description | Required | Default | +|---|--|---|---|---| +| `--config` | - |The file path to the MyWhoop [configuration file](./docs/configuration_reference.md). | No | `~/.mywhoop.yaml` | +| `--credentials` | - |The file path to the Whoop credentials file that contains a valid Whoop authentication token. | No | `token.json` | +| `--debug` | `-d` | Modify the output logging level. | No | `INFO` | -```shell -nohup ./mywhoop server -d DEBUG > output.log 2>&1 & + +> [!IMPORTANT] +> For more information on the MyWhoop configuration file, refer to the [Configuration Reference](./docs/configuration_reference.md) section. + + + +### Dump + +The dump command downloads **all your Whoop data** and saves it to a local file. For more advanced configurations, use a Mywhoop configuration file. For more information, refer to the [Configuration Reference](./docs/configuration_reference.md) section. + +```bash +mywhoop dump ``` -## Overview +| Long Flag | Short Flag | Description | Required | Default | +|---|---|---|---|---| +| `--location` | `-l` |The location to save the Whoop data file. | No | `./data/` | -MyWhoop was created to help faciliate ownership of your data and allow you to interfact with your own data in different ways than what Whoop may offer. +> [!IMPORTANT] +> MyWhoop has exponential backoff and retries logic built in for the Whoop API. If the API is down or the request fails, MyWhoop will retry the request. Whoop has an [API rate limit of 100 requests per minute](https://developer.whoop.com/docs/developing/rate-limiting). If the rate limit is exceeded, MyWhoop will attempt to retry the request after a delay for up to a maximum of 5 minutes. If the request fails after 5 minutes, the application will exit with an error. If the Whoop API rejects the authentication token, the application will exit with an error. -## Environment Variables -The following environment variables are available for use with MyWhoop. +### Login -| Variable | Description | Required | -|---|----|---| -| `WHOOP_CLIENT_ID` | The client ID for your Whoop application. | Yes | -| `WHOOP_CLIENT_SECRET` | The client secret for your Whoop application. | Yes | -| `WHOOP_CREDENTIALS_FILE` | The file path to the Whoop credentials file that contains a valid Whoop authentication token. Default value is `token.json`. | No | +The login command is used to authenticate with the Whoop API and save the authentication token locally. The command will set up a local HTTP server to handle the OAuth2 handshake with the Whoop API and save the token to a local file. +```bash +mywhoop login +``` + +| Long Flag | Short Flag |Description | Required | Default | +|---|--|--|---|---| +| `--no-auto-open` | `-n` |By default, the login command will automatically open a browser window to the Whoop login page. Use this flag to disable this behavior. | No | False | +| `--port` | `-p` | The port to use for the local HTTP server. | No | `8080` | +| `--redirect-url` | `-r` |The redirect URL to use for the OAuth2 handshake. | No | `http://localhost:8080/redirect`. | + + + +## Server + +The server command automatically downloads your Whoop data daily. If specified, it saves or exports the data to a local file or a remote location. It is designed to be started as a background process and will automatically download your Whoop data daily. The command will refresh the Whoop authentication token every 55 minutes and update the local token file. The Whoop API is queried precisely every 24 hours from when the server is started. + +Use a MyWhoop configuration file for more advanced configurations. For more information, refer to the [Configuration Reference](./docs/configuration_reference.md) section. -### Notification Variables -Depending on the notification service you use, you may need to provide additional environment variables. +| Long Flag | Short Flag |Description | Required | Default | +|---|--|--|---|---| +| `--first-run-download` | - |Download all the available Whoop data on the first run. | No | False | -| Variable | Description | Required | -|---|----|---| -| `NOTIFICATION_NTFY_AUTH_TOKEN`| The token for the [Ntfy](https://docs.ntfy.sh/) service. Required if the ntfy subscription requires a token. | No | -| `NOTIFICATION_NTFY_PASSWORD` | The password for the ntfy subscription if username/password authentication is used. Required if the ntfy subscription requires a username and password. | No | -### Order Of Precendence +```bash +mywhoop server +``` + + +## Version + +The version command is used to display the version of MyWhoop. The version command checks for the latest version of MyWhoop and displays the current version. If a new version is available, the command will notify you. -The order of precendence is as follows. -1. CLI flags. -2. Environment Variables -3. Configuration File +```bash +mywhoop version +``` +``` +2024/07/06 10:50:29 INFO mywhoop v1.0.0 +``` diff --git a/cmd/dump.go b/cmd/dump.go index 08fde48..a650055 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -16,8 +16,9 @@ import ( "github.com/spf13/cobra" ) -// meCmd represents the me command -var meCmd = &cobra.Command{ +var dataLocation string + +var dumpCmd = &cobra.Command{ Use: "dump", Short: "Dump all your Whoop data to a file or another form of export.", Long: "Dump all your Whoop data to a file or another form of export.", @@ -29,7 +30,8 @@ var meCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(meCmd) + dumpCmd.PersistentFlags().StringVarP(&dataLocation, "location", "l", "", "The location to dump the data to. Default is the current directory's data/ folder.") + rootCmd.AddCommand(dumpCmd) } func dump(ctx context.Context) error { @@ -161,10 +163,18 @@ func dump(ctx context.Context) error { } return err } + var filePath string switch cfg.Export.Method { case "file": - fileExp := export.NewFileExport(Configuration.Export.FileExport.FilePath, + + if dataLocation == "" { + filePath = Configuration.Export.FileExport.FilePath + } else { + filePath = dataLocation + } + + fileExp := export.NewFileExport(filePath, Configuration.Export.FileExport.FileType, Configuration.Export.FileExport.FileName, Configuration.Export.FileExport.FileNamePrefix, @@ -180,6 +190,11 @@ func dump(ctx context.Context) error { } slog.Info("Data exported successfully", "file", fileExp.FileName) case "s3": + + if dataLocation != "" { + cfg.Export.AWSS3.FileConfig.FilePath = dataLocation + } + awsS3, err := export.NewAwsS3Export(cfg.Export.AWSS3.Region, cfg.Export.AWSS3.Bucket, cfg.Export.AWSS3.Profile, @@ -200,8 +215,15 @@ func dump(ctx context.Context) error { } default: + + if dataLocation == "" { + filePath = Configuration.Export.FileExport.FilePath + } else { + filePath = dataLocation + } + slog.Info("no export method specified. Defaulting to file.") - fileExp := export.NewFileExport(Configuration.Export.FileExport.FilePath, + fileExp := export.NewFileExport(filePath, Configuration.Export.FileExport.FileType, Configuration.Export.FileExport.FileName, Configuration.Export.FileExport.FileNamePrefix, diff --git a/cmd/login.go b/cmd/login.go index d8fb5a9..70bf3dd 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -40,7 +40,7 @@ var ( ) func init() { - loginCmd.PersistentFlags().BoolVarP(&noAutoOpenBrowser, "no-auto", "n", false, "Do not automatically open the browser to authenticate with the Whoop API. ") + loginCmd.PersistentFlags().BoolVarP(&noAutoOpenBrowser, "no-auto-open", "n", false, "Do not automatically open the browser to authenticate with the Whoop API. ") loginCmd.PersistentFlags().StringVarP(&redirectURL, "redirect-url", "r", "/redirect", "The URL path to redirect to after authenticating with the Whoop API. Default is path is /redirect.") loginCmd.PersistentFlags().StringVarP(&port, "port", "p", "8080", "The port to listen on. Default is 8080.") rootCmd.AddCommand(loginCmd) @@ -177,6 +177,7 @@ func redirectHandler(assets fs.FS, page, errorPage string, authConf *oauth2.Conf err = internal.WriteLocalToken(credentialsFilePath, accessToken) if err != nil { + slog.Debug("Credentials file path", "path", credentialsFilePath) slog.Error("unable to write token to file", "error", err) err := sendErrorTemplate(w, err.Error(), http.StatusInternalServerError, errorPage, assets) if err != nil { @@ -218,6 +219,7 @@ func closeHandler(w http.ResponseWriter, r *http.Request) { if err != nil { slog.Error("unable to write response", "error", err) } + defer os.Exit(0) } @@ -256,10 +258,6 @@ func openBrowser(url string, disableCmd bool) error { // getErrorTemplate returns an HTML template from a file func getErrorTemplate(assets fs.FS, file string) (*template.Template, error) { - if _, err := os.Stat(file); os.IsNotExist(err) { - return nil, err - } - t, err := template.ParseFS(assets, file) if err != nil { return nil, err diff --git a/cmd/root.go b/cmd/root.go index 073be44..2f91af4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -98,6 +98,7 @@ func InitLogger(cfg *internal.ConfigurationData) error { // Prioritize CLI flags if CredentialsFile != "" { + slog.Info("User provided credentials file path", "path", CredentialsFile) cfg.Credentials.CredentialsFile = CredentialsFile } @@ -112,8 +113,10 @@ func InitLogger(cfg *internal.ConfigurationData) error { ) if cfg.Credentials.CredentialsFile == "" { + slog.Info("No credentials file provided. Using default credentials file") cfg.Credentials.CredentialsFile = internal.DEFAULT_CREDENTIALS_FILE } + slog.Debug("Configuration", "Config", cfg) return nil diff --git a/docs/configuration_reference.md b/docs/configuration_reference.md new file mode 100644 index 0000000..9fbe136 --- /dev/null +++ b/docs/configuration_reference.md @@ -0,0 +1,188 @@ +# Configuration Reference + +The MyWhoop configuration file is used to configure MyWhoop's behavior and enable advanced features. By default, it is a YAML file located at `~/.mywhoop.yaml`. You can override the configuration file by using CLI flags. + + +> [!IMPORTANT] +> You can learn more about supported environment variables in the [Environment Variables](./environment_variables.md) section. + + + +## Credentials + +The credentials section of the configuration file is used to configure where the MyWhoop authentication token is stored or where to find it. The following fields are available for configuration: + +| Field | Description | Required | Default | +|---|----|---|---| +| `credentialsFile` | The file path to the Whoop credentials file that contains a valid Whoop authentication token. By default, MyWhoop looks for a token file in the local directory. | No | `token.json` | + +```yaml +credentials: + credentialsFile: "/opt/mywhoop/token.json" +``` + +## Debug + +The debug section of the configuration file is used to enable debug logging for MyWhoop. The following fields are available for configuration: + +| Field | Description | Required | Default | +|---|----|---|---| +| `debug` | Enable debug logging. Allowed values are: `INFO`, `DEBUG`, `WARN`, and `ERROR`. | No | `INFO` | + + +```yaml +debug: "info" +``` + +## Export + +The export section of the configuration file is used to configure the data export feature of MyWhoop. The export feature allows you to export your Whoop data to a remote location such as an S3 bucket. The following fields are available for configuration: + +| Field | Description | Required | Default | +|---|----|---|---| +| `method` | The export method to use. Allowed values are `file` and `s3`. | Yes | | +| `fileExport` | The file export configuration. Required if `method` is `file`. | No | | +| `s3Export` | The S3 export configuration. Required if `method` is `s3`. | No | | + + +### File Export + +Local file export accepts the following fields. + +| Field | Description | Required | Default | +|---|----|---|---| +| `fileName` | The name of the file to export. | Yes | `user` | +| `filePath` | The path to save the file. By default, a data folder is created in the immediate folder. | Yes | `data/` | +| `fileType` | The file type to save the file as. Allowed values are `json`. | Yes | `json` | +| `fileNamePrefix` | The prefix to add to the file name. In server mode, the data is automatically inserted as a prefix. | No |`""` | +| `serverMode` | Ensures the file name is unique and contains a timestamp. Ensures behaviors match server mode. | No | `false` | + +```yaml +export: + method: file + fileExport: + fileName: "user" + filePath: "app/" + fileType: "json" + fileNamePrefix: "" +``` + +### S3 Export + +You can export your Whoop data to an AWS S3 bucket using the S3 export method. By default, AWS credentials are honored from the environment variables or the default profile in the [order of precedence of the AWS SDK](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials). The S3 export method accepts the following fields: + +| Field | Description | Required | Default | +|---|----|---|---| +| `bucket` | The S3 bucket to export the data to. | Yes | | +| `region` | The AWS region to use. | Yes | | +| `profile` | The AWS profile to use. | No | | +| `fileConfig` | The file configuration for the S3 export. Refer to the [file export configuration](#configuration-reference) for more information. | Yes | | + + +```yaml +export: + method: s3 + awsS3: + region: "us-east-1" + bucket: "my-example-bucket" + fileConfig: + fileName: "" + fileType: "json" + fileNamePrefix: "" + serverMode: true +``` + +## Notification + +The notification section of the configuration file is used to configure the notification feature of MyWhoop. By default, notifications are sent to stdout. The notification feature allows you to use a different service to receive notifications when MyWhoop completes a task. The following fields are available for configuration: + +| Field | Description | Required | Default | +|---|----|---|---| +|`method` | The notification method to use. Allowed values are `ntfy`, or `""`. | Yes | `""`| +| `ntfy` | The ntfy notification configuration. Required if `method` is `ntfy`. | No | | + +### Ntfy + +You can use the open-source service, [Ntfy](https://docs.ntfy.sh/), to receive notifications when MyWhoop completes a task or an error is encountered. The Ntfy configuration block accepts the following fields: + +| Field | Description | Required | Default | +|---|----|---|---| +| `events` | The events to receive notifications for. Allowed values are `""`, `all`, `success`, and `error`. | Yes | `all` | +| `serverEndpoint` | The Ntfy server endpoint to send notifications to. | Yes | `""` | +| `subscriptionID` | The subscription ID to use for the Ntfy subscription. | Yes | `""` | +| `userName` | The username for the Ntfy subscription. Required if user name and password is used. | No | `""` | + +```yaml +notification: + method: "ntfy" + ntfy: + serverEndpoint: "https://example.my.ntfy.com" + subscriptionID: "mywhoop_custom_notifications" + events: "all" +``` + +> [!IMPORTANT] +> Use the environment variables `NOTIFICATION_NTFY_AUTH_TOKEN` or `NOTIFICATION_NTFY_PASSWORD` to provide the Ntfy authentication credentials. + + + +## Server + +The server section of the configuration file is used to configure the server feature of MyWhoop. The server feature allows you to start MyWhoop as a server that queries the Whoop API every 24 hours. The following fields are available for configuration: + +| Field | Description | Required | Default | +|---|----|---|---| +| `enabled` | Enable the server feature. | No | `false` | +| `firstRunDownload` | Download the data on the first run. This is not a recommended flag for server mode as it will re-download all the Whoop data on a server or machine reboot. | No | `false` | + + +```yaml +server: + enabled: true + firstRunDownload: false +``` + + +## Example Configuration File + + +The following is an example configuration file that configures MyWhoop to export data to an S3 bucket, enable the server feature, and send notifications using the Ntfy service. + +```yaml +export: + method: s3 + awsS3: + region: "us-east-1" + bucket: "42acg-primary-data-whoop-bucket" + fileConfig: + fileName: "" + fileType: "json" + fileNamePrefix: "" + serverMode: true +server: + enabled: true + firstRunDownload: false +notification: + method: "ntfy" + ntfy: + serverEndpoint: "https://ntfy.self-hosted.example" + subscriptionID: "mywhoop_alerts_at_home" + events: "all" +debug: info +``` + + +The following is an example configuration file configuring MyWhoop to export data to a local file and enabling debug logging. + +```yaml +export: + method: file + fileExport: + fileName: "user" + filePath: "/opt/mywhoop/data/" + fileType: "json" + fileNamePrefix: "" +credentials: + credentialsFile: "/opt/mywhoop/token.json" +debug: debug +``` \ No newline at end of file diff --git a/docs/environment_variables.md b/docs/environment_variables.md new file mode 100644 index 0000000..60974d8 --- /dev/null +++ b/docs/environment_variables.md @@ -0,0 +1,35 @@ +# Environment Variables + + +MyWhoop uses environment variables to configure the application. The environment variables are used to configure the Whoop API client and other integrations such as notifications. + +### Order Of Precendence + +The order of precendence is as follows. + +1. CLI flags. +2. Environment Variables +3. Configuration File + + +## MyWhoop Variables + +The following environment variables are used to configure MyWhoop. The two required variables are `WHOOP_CLIENT_ID` and `WHOOP_CLIENT_SECRET`. + + +| Variable | Description | Required | +|---|----|---| +| `WHOOP_CLIENT_ID` | The client ID for your Whoop application. | Yes | +| `WHOOP_CLIENT_SECRET` | The client secret for your Whoop application. | Yes | +| `WHOOP_CREDENTIALS_FILE` | The file path to the Whoop credentials file that contains a valid Whoop authentication token. Default value is `token.json`. | No | + + +### Notification Variables + +Depending on the notification service you use, you may need to provide additional environment variables. + +| Variable | Description | Required | +|---|----|---| +| `NOTIFICATION_NTFY_AUTH_TOKEN`| The token for the [Ntfy](https://docs.ntfy.sh/) service. Required if the ntfy subscription requires a token. | No | +| `NOTIFICATION_NTFY_PASSWORD` | The password for the ntfy subscription if username/password authentication is used. Required if the ntfy subscription requires a username and password. | No | + diff --git a/documentation/examples/systemd/mywhoop_binary.service b/docs/examples/systemd/mywhoop_binary.service similarity index 100% rename from documentation/examples/systemd/mywhoop_binary.service rename to docs/examples/systemd/mywhoop_binary.service diff --git a/docs/get-started.md b/docs/get-started.md new file mode 100644 index 0000000..9720e1a --- /dev/null +++ b/docs/get-started.md @@ -0,0 +1,152 @@ +# Get Started + +MyWhoop requires a few one-time setup steps to get started. The primary steps include setting up a Whoop application in the Whoop Developer Portal and setting up MyWhoop on your local machine or server. Once you have completed these steps, you can start using MyWhoop to interact with your Whoop data as you see fit. + + +The following steps will guide you through the process of setting up MyWhoop on your local machine or server. + +## Prerequisites + +1. [Docker](https://docs.docker.com/get-docker/) installed on your local machine or server. You can use Podman as an alternative to Docker. This guide will use Docker as the container runtime. + +2. A Whoop account. You need to use your Whoop account to create a new application in the Whoop Developer Portal. + +3. A Linux or MacOS environment. If you are using Windows, consider using Windows Subsystem for Linux (WSL2) for a better user experience. + +> [!NOTE] +> You can use MyWhoop on Windows natively through the Windows binary of MyWhoop. However, this guide will focus on using MyWhoop with Docker. + +4. A web browser to access the Whoop Developer Portal and login to your Whoop account. You will use a web browser to create the JSON Web Token (JWT) token required to interact with the Whoop API. + + +## Steps + +1. Open a web browser session and log into the [Whoop Developer Portal](https://developer-dashboard.whoop.com/login) + + +2. Click on the `Create New Application` button to start creating a new application. + + +3. Fill out the application details. You can use the following details as a guide to fill out the application details. + + + | Field | Description | Required | Example | + |---|----|---|--| + | Name | The name of your application. | Yes | `my-custom-app` | + | Logo` | The logo for your application. | No | - | + | Contacts | The contact information for your application. | Yes | `email@example.com` | + | Privacy Policy | The URL to your application's privacy policy. | No | `https://example.com/privacy` | + | Redirect URIs | The redirect URIs for your application. | Yes | `http://localhost:8080/redirect, https://localhost:8080/redirect` | + | Scopes | The scopes required for your application. | Yes | `read:recovery, read:cycles,read:sleep, read:workout, read:profile, read:body_measurement` | + | Webhook URL| The URL for the webhook endpoint. | No | - | + +> [!NOTE] +> The redirect URIs are required for the Whoop authentication flow. Use the values `http://localhost:8080/redirect, https://localhost:8080/` as the redirect UTLs. You can change the port number if you are using a different port. + + ![A view of the application create page](../static/images/tutorial_1.png) + + +4. Click on the `Create Application` button to create the application. You will be redirected to the application details page. + +5. Copy the `Client ID` and `Client Secret` from the application details page. You will use these values to authenticate with the Whoop API. + +> [!WARNING] +> These values are sensitive and should be kept secret. Do not share these values with anyone. I recommend storing these values in a secure location such as a password manager. + + + ![A view of an application with the Whoop App clientID and secretId highlighted](../static/images/tutorial_2.png) + +6. Open a terminal window and set the `WHOOP_CLIENT_ID` and `WHOOP_CLIENT_SECRET` environment variables with the values you copied from the Whoop Developer Portal. + + + ```shell + export WHOOP_CLIENT_ID=Status Code: {{.StatusCode}}
+Status Code: {{.StatusCode}}
{{.Error}}
+{{.Error}}