@dlwiest/ts-tonal-client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/client.d.ts +16 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.esm.js +147 -0
- package/dist/index.js +149 -0
- package/dist/types.d.ts +115 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Derrick Wiest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Tonal Client
|
|
2
|
+
|
|
3
|
+
This repository contains a TypeScript client for accessing Tonal data. It is designed to interact with Tonal's systems to retrieve various types of workout-related data.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Retrieve detailed workout data by ID.
|
|
9
|
+
- Access shared workouts through share URLs.
|
|
10
|
+
- Fetch movements and other related workout data.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
To use this client, clone the repository and install the necessary dependencies:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git clone https://github.com/dlwiest/ts-tonal-client.git
|
|
18
|
+
cd ts-tonal-client
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
In order to test with the example scripts in `/src/examples` you will need to create a `.env` file based on the formatting described in `.env.sample`.
|
|
23
|
+
Note that your Tonal username is most likely an email address.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
The client can be used in scripts or applications that require data from Tonal. Here are some examples of how to use the client:
|
|
28
|
+
|
|
29
|
+
### Get Movements
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Example: Fetching movements
|
|
33
|
+
import { TonalClient } from './src/client';
|
|
34
|
+
|
|
35
|
+
async function fetchMovements() {
|
|
36
|
+
const client = await TonalClient.create({ username: 'your_username', password: 'your_password' });
|
|
37
|
+
const movements = await client.getMovements();
|
|
38
|
+
console.log(movements);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fetchMovements();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Get Workout by ID
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// Example: Fetching a workout by ID
|
|
48
|
+
import { TonalClient } from './src/client';
|
|
49
|
+
|
|
50
|
+
async function fetchWorkoutById(workoutId: string) {
|
|
51
|
+
const client = await TonalClient.create({ username: 'your_username', password: 'your_password' });
|
|
52
|
+
const workout = await client.getWorkoutById(workoutId);
|
|
53
|
+
console.log(workout);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fetchWorkoutById('workout_id_here');
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Get Workout by Share URL
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Example: Fetching a workout by its share URL
|
|
63
|
+
import { TonalClient } from './src/client';
|
|
64
|
+
|
|
65
|
+
async function fetchWorkoutByShareUrl(shareUrl: string) {
|
|
66
|
+
const client = await TonalClient.create({ username: 'your_username', password: 'your_password' });
|
|
67
|
+
const workout = await client.getWorkoutByShareUrl(shareUrl);
|
|
68
|
+
console.log(workout);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fetchWorkoutByShareUrl('https://link.tonal.com/custom-workout/your_workout_id');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Contributing
|
|
75
|
+
|
|
76
|
+
Contributions to this project are welcome, especially in the areas of error handling, expanding functionality, and improving the robustness of the client.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
This project is open-sourced under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
81
|
+
|
|
82
|
+
## Contact
|
|
83
|
+
|
|
84
|
+
For any queries or further assistance, please contact [Derrick Wiest](mailto:me@dlwiest.com).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TonalMovement, TonalSharedWorkout, TonalWorkout } from './types';
|
|
2
|
+
export declare class TonalClient {
|
|
3
|
+
private username;
|
|
4
|
+
private password;
|
|
5
|
+
private idToken;
|
|
6
|
+
private tokenExpiresAt;
|
|
7
|
+
private constructor();
|
|
8
|
+
static create({ username, password }: {
|
|
9
|
+
username: string;
|
|
10
|
+
password: string;
|
|
11
|
+
}): Promise<TonalClient>;
|
|
12
|
+
private refreshToken;
|
|
13
|
+
getMovements(): Promise<TonalMovement[]>;
|
|
14
|
+
getWorkoutById(id: string): Promise<TonalWorkout>;
|
|
15
|
+
getWorkoutByShareUrl(shareUrl: string): Promise<TonalSharedWorkout>;
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
interface OAuthTokenResponse {
|
|
2
|
+
access_token: string;
|
|
3
|
+
id_token: string;
|
|
4
|
+
refresh_token: string;
|
|
5
|
+
scope: string;
|
|
6
|
+
token_type: string;
|
|
7
|
+
expires_in: number;
|
|
8
|
+
}
|
|
9
|
+
type MuscleGroup = 'Obliques' | 'Abs' | 'Shoulders' | 'Glutes' | 'Back' | 'Biceps' | 'Quads' | 'Triceps' | 'Chest' | 'Hamstrings' | 'Calves' | 'Forearms';
|
|
10
|
+
interface TonalMovement {
|
|
11
|
+
id: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
name: string;
|
|
15
|
+
shortName: string;
|
|
16
|
+
muscleGroups: MuscleGroup[];
|
|
17
|
+
bodyRegion: string;
|
|
18
|
+
bodyRegionDisplay: string;
|
|
19
|
+
baseOfSupport: string;
|
|
20
|
+
pushPull: string;
|
|
21
|
+
family: string;
|
|
22
|
+
familyDisplay: string;
|
|
23
|
+
inFreeLift: boolean;
|
|
24
|
+
onMachine: boolean;
|
|
25
|
+
countReps: boolean;
|
|
26
|
+
isTwoSided: boolean;
|
|
27
|
+
isBilateral: boolean;
|
|
28
|
+
isAlternating: boolean;
|
|
29
|
+
offMachineAccessory: string;
|
|
30
|
+
descriptionHow: string;
|
|
31
|
+
descriptionWhy: string;
|
|
32
|
+
sortOrder: number;
|
|
33
|
+
imageAssetId: string;
|
|
34
|
+
skillLevel: number;
|
|
35
|
+
active: boolean;
|
|
36
|
+
featureGroupIds: null | string[];
|
|
37
|
+
isGeneric: boolean;
|
|
38
|
+
}
|
|
39
|
+
interface TonalWorkout {
|
|
40
|
+
id: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
title: string;
|
|
43
|
+
shortDescription: string;
|
|
44
|
+
description: string;
|
|
45
|
+
productionCode: string;
|
|
46
|
+
assetId: string;
|
|
47
|
+
coachId: string;
|
|
48
|
+
sets: WorkoutSet[];
|
|
49
|
+
duration: number;
|
|
50
|
+
publishState: string;
|
|
51
|
+
programId: string | null;
|
|
52
|
+
level: string;
|
|
53
|
+
groupIds: string[];
|
|
54
|
+
targetArea: string;
|
|
55
|
+
tags: string[] | null;
|
|
56
|
+
bodyRegions: MuscleGroup[];
|
|
57
|
+
goalIds: string[] | null;
|
|
58
|
+
trainingEffectGoals: string[];
|
|
59
|
+
disableModification: boolean;
|
|
60
|
+
publishedAt: string;
|
|
61
|
+
localPublishedAt: string;
|
|
62
|
+
type: string;
|
|
63
|
+
userId: string;
|
|
64
|
+
style: string;
|
|
65
|
+
trainingType: string;
|
|
66
|
+
trainingTypeIds: string[] | null;
|
|
67
|
+
mobileFriendly: boolean;
|
|
68
|
+
live: boolean;
|
|
69
|
+
recoveryWeight: boolean;
|
|
70
|
+
supportedDevices: string[] | null;
|
|
71
|
+
featureGroupIds: string[] | null;
|
|
72
|
+
movementIds: string[];
|
|
73
|
+
muscleGroupsForExclusion: MuscleGroup[] | null;
|
|
74
|
+
playbackType: string;
|
|
75
|
+
isImported: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface WorkoutSet {
|
|
78
|
+
id: string;
|
|
79
|
+
workoutId: string;
|
|
80
|
+
blockStart: boolean;
|
|
81
|
+
movementId: string;
|
|
82
|
+
prescribedReps: number;
|
|
83
|
+
repetition: number;
|
|
84
|
+
repetitionTotal: number;
|
|
85
|
+
blockNumber: number;
|
|
86
|
+
burnout: boolean;
|
|
87
|
+
spotter: boolean;
|
|
88
|
+
eccentric: boolean;
|
|
89
|
+
chains: boolean;
|
|
90
|
+
skipSetup: boolean;
|
|
91
|
+
skipDemo: boolean;
|
|
92
|
+
finalSet: boolean;
|
|
93
|
+
calibration: boolean;
|
|
94
|
+
practice: boolean;
|
|
95
|
+
flex: boolean;
|
|
96
|
+
progressive: boolean;
|
|
97
|
+
weightPercentage: number;
|
|
98
|
+
warmUp: boolean;
|
|
99
|
+
durationBasedRepGoal: number;
|
|
100
|
+
setGroup: number;
|
|
101
|
+
round: number;
|
|
102
|
+
description: string;
|
|
103
|
+
dropSet: boolean;
|
|
104
|
+
omitempty: null | any;
|
|
105
|
+
}
|
|
106
|
+
interface TonalSharedWorkout {
|
|
107
|
+
id: string;
|
|
108
|
+
sharerUserId: string;
|
|
109
|
+
parentWorkoutId: string;
|
|
110
|
+
workoutSnapshotId: string;
|
|
111
|
+
workoutSnapshotHash: string;
|
|
112
|
+
deepLinkUrl: string;
|
|
113
|
+
workoutSnapshot: TonalWorkout;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
declare class TonalClient {
|
|
117
|
+
private username;
|
|
118
|
+
private password;
|
|
119
|
+
private idToken;
|
|
120
|
+
private tokenExpiresAt;
|
|
121
|
+
private constructor();
|
|
122
|
+
static create({ username, password }: {
|
|
123
|
+
username: string;
|
|
124
|
+
password: string;
|
|
125
|
+
}): Promise<TonalClient>;
|
|
126
|
+
private refreshToken;
|
|
127
|
+
getMovements(): Promise<TonalMovement[]>;
|
|
128
|
+
getWorkoutById(id: string): Promise<TonalWorkout>;
|
|
129
|
+
getWorkoutByShareUrl(shareUrl: string): Promise<TonalSharedWorkout>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { MuscleGroup, OAuthTokenResponse, TonalMovement, TonalSharedWorkout, TonalWorkout, TonalClient as default };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
|
|
4
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
5
|
+
purpose with or without fee is hereby granted.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
8
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
9
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
10
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
11
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
12
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
13
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
14
|
+
***************************************************************************** */
|
|
15
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
19
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
20
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
21
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
22
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
23
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
24
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
29
|
+
var e = new Error(message);
|
|
30
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class TonalClient {
|
|
34
|
+
constructor({ username, password }) {
|
|
35
|
+
this.username = username;
|
|
36
|
+
this.password = password;
|
|
37
|
+
this.idToken = '';
|
|
38
|
+
this.tokenExpiresAt = 0;
|
|
39
|
+
}
|
|
40
|
+
// TonalClient factory
|
|
41
|
+
static create(_a) {
|
|
42
|
+
return __awaiter(this, arguments, void 0, function* ({ username, password }) {
|
|
43
|
+
const client = new TonalClient({ username, password });
|
|
44
|
+
yield client.refreshToken();
|
|
45
|
+
return client;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Request a new ID token from Auth0
|
|
49
|
+
refreshToken() {
|
|
50
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
const data = {
|
|
52
|
+
username: this.username,
|
|
53
|
+
password: this.password,
|
|
54
|
+
client_id: 'ERCyexW-xoVG_Yy3RDe-eV4xsOnRHP6L',
|
|
55
|
+
grant_type: 'password',
|
|
56
|
+
scope: 'offline_access',
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const response = yield fetch('https://tonal.auth0.com/oauth/token', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(data),
|
|
65
|
+
});
|
|
66
|
+
if (response.status === 403) {
|
|
67
|
+
throw new Error('Invalid username or password');
|
|
68
|
+
}
|
|
69
|
+
const json = (yield response.json());
|
|
70
|
+
this.idToken = json.id_token;
|
|
71
|
+
this.tokenExpiresAt = Date.now() + json.expires_in * 1000;
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.error(e);
|
|
75
|
+
throw new Error('Failed to retrieve access token');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Get all movements available on Tonal
|
|
80
|
+
getMovements() {
|
|
81
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
try {
|
|
83
|
+
if (this.tokenExpiresAt < Date.now()) {
|
|
84
|
+
yield this.refreshToken();
|
|
85
|
+
}
|
|
86
|
+
const response = yield fetch('https://api.tonal.com/v6/movements', {
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${this.idToken}`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return (yield response.json());
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
console.error(e);
|
|
95
|
+
throw new Error('Failed to retrieve movements');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Get a workout by its ID
|
|
100
|
+
getWorkoutById(id) {
|
|
101
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
try {
|
|
103
|
+
if (this.tokenExpiresAt < Date.now()) {
|
|
104
|
+
yield this.refreshToken();
|
|
105
|
+
}
|
|
106
|
+
const response = yield fetch(`https://api.tonal.com/v6/workouts/${id}`, {
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${this.idToken}`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const json = (yield response.json());
|
|
112
|
+
return json;
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.error(e);
|
|
116
|
+
throw new Error('Failed to retrieve workout');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Get a workout by its share URL
|
|
121
|
+
getWorkoutByShareUrl(shareUrl) {
|
|
122
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
if (!shareUrl) {
|
|
124
|
+
throw new Error('Share URL is required');
|
|
125
|
+
}
|
|
126
|
+
const shareId = shareUrl.split('/').pop();
|
|
127
|
+
try {
|
|
128
|
+
if (this.tokenExpiresAt < Date.now()) {
|
|
129
|
+
yield this.refreshToken();
|
|
130
|
+
}
|
|
131
|
+
const response = yield fetch(`https://api.tonal.com/v6/user-workouts/sharing-records/${shareId}`, {
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${this.idToken}`,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const json = (yield response.json());
|
|
137
|
+
return json;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
console.error(e);
|
|
141
|
+
throw new Error('Failed to retrieve workout');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { TonalClient as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/******************************************************************************
|
|
4
|
+
Copyright (c) Microsoft Corporation.
|
|
5
|
+
|
|
6
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
7
|
+
purpose with or without fee is hereby granted.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
***************************************************************************** */
|
|
17
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
21
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
22
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
23
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
24
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
25
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
26
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
31
|
+
var e = new Error(message);
|
|
32
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class TonalClient {
|
|
36
|
+
constructor({ username, password }) {
|
|
37
|
+
this.username = username;
|
|
38
|
+
this.password = password;
|
|
39
|
+
this.idToken = '';
|
|
40
|
+
this.tokenExpiresAt = 0;
|
|
41
|
+
}
|
|
42
|
+
// TonalClient factory
|
|
43
|
+
static create(_a) {
|
|
44
|
+
return __awaiter(this, arguments, void 0, function* ({ username, password }) {
|
|
45
|
+
const client = new TonalClient({ username, password });
|
|
46
|
+
yield client.refreshToken();
|
|
47
|
+
return client;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Request a new ID token from Auth0
|
|
51
|
+
refreshToken() {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
const data = {
|
|
54
|
+
username: this.username,
|
|
55
|
+
password: this.password,
|
|
56
|
+
client_id: 'ERCyexW-xoVG_Yy3RDe-eV4xsOnRHP6L',
|
|
57
|
+
grant_type: 'password',
|
|
58
|
+
scope: 'offline_access',
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
const response = yield fetch('https://tonal.auth0.com/oauth/token', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(data),
|
|
67
|
+
});
|
|
68
|
+
if (response.status === 403) {
|
|
69
|
+
throw new Error('Invalid username or password');
|
|
70
|
+
}
|
|
71
|
+
const json = (yield response.json());
|
|
72
|
+
this.idToken = json.id_token;
|
|
73
|
+
this.tokenExpiresAt = Date.now() + json.expires_in * 1000;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
console.error(e);
|
|
77
|
+
throw new Error('Failed to retrieve access token');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Get all movements available on Tonal
|
|
82
|
+
getMovements() {
|
|
83
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
try {
|
|
85
|
+
if (this.tokenExpiresAt < Date.now()) {
|
|
86
|
+
yield this.refreshToken();
|
|
87
|
+
}
|
|
88
|
+
const response = yield fetch('https://api.tonal.com/v6/movements', {
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${this.idToken}`,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return (yield response.json());
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.error(e);
|
|
97
|
+
throw new Error('Failed to retrieve movements');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Get a workout by its ID
|
|
102
|
+
getWorkoutById(id) {
|
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
try {
|
|
105
|
+
if (this.tokenExpiresAt < Date.now()) {
|
|
106
|
+
yield this.refreshToken();
|
|
107
|
+
}
|
|
108
|
+
const response = yield fetch(`https://api.tonal.com/v6/workouts/${id}`, {
|
|
109
|
+
headers: {
|
|
110
|
+
Authorization: `Bearer ${this.idToken}`,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const json = (yield response.json());
|
|
114
|
+
return json;
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
console.error(e);
|
|
118
|
+
throw new Error('Failed to retrieve workout');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Get a workout by its share URL
|
|
123
|
+
getWorkoutByShareUrl(shareUrl) {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
if (!shareUrl) {
|
|
126
|
+
throw new Error('Share URL is required');
|
|
127
|
+
}
|
|
128
|
+
const shareId = shareUrl.split('/').pop();
|
|
129
|
+
try {
|
|
130
|
+
if (this.tokenExpiresAt < Date.now()) {
|
|
131
|
+
yield this.refreshToken();
|
|
132
|
+
}
|
|
133
|
+
const response = yield fetch(`https://api.tonal.com/v6/user-workouts/sharing-records/${shareId}`, {
|
|
134
|
+
headers: {
|
|
135
|
+
Authorization: `Bearer ${this.idToken}`,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const json = (yield response.json());
|
|
139
|
+
return json;
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.error(e);
|
|
143
|
+
throw new Error('Failed to retrieve workout');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = TonalClient;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface OAuthTokenResponse {
|
|
2
|
+
access_token: string;
|
|
3
|
+
id_token: string;
|
|
4
|
+
refresh_token: string;
|
|
5
|
+
scope: string;
|
|
6
|
+
token_type: string;
|
|
7
|
+
expires_in: number;
|
|
8
|
+
}
|
|
9
|
+
export type MuscleGroup = 'Obliques' | 'Abs' | 'Shoulders' | 'Glutes' | 'Back' | 'Biceps' | 'Quads' | 'Triceps' | 'Chest' | 'Hamstrings' | 'Calves' | 'Forearms';
|
|
10
|
+
export interface TonalMovement {
|
|
11
|
+
id: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
name: string;
|
|
15
|
+
shortName: string;
|
|
16
|
+
muscleGroups: MuscleGroup[];
|
|
17
|
+
bodyRegion: string;
|
|
18
|
+
bodyRegionDisplay: string;
|
|
19
|
+
baseOfSupport: string;
|
|
20
|
+
pushPull: string;
|
|
21
|
+
family: string;
|
|
22
|
+
familyDisplay: string;
|
|
23
|
+
inFreeLift: boolean;
|
|
24
|
+
onMachine: boolean;
|
|
25
|
+
countReps: boolean;
|
|
26
|
+
isTwoSided: boolean;
|
|
27
|
+
isBilateral: boolean;
|
|
28
|
+
isAlternating: boolean;
|
|
29
|
+
offMachineAccessory: string;
|
|
30
|
+
descriptionHow: string;
|
|
31
|
+
descriptionWhy: string;
|
|
32
|
+
sortOrder: number;
|
|
33
|
+
imageAssetId: string;
|
|
34
|
+
skillLevel: number;
|
|
35
|
+
active: boolean;
|
|
36
|
+
featureGroupIds: null | string[];
|
|
37
|
+
isGeneric: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface TonalWorkout {
|
|
40
|
+
id: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
title: string;
|
|
43
|
+
shortDescription: string;
|
|
44
|
+
description: string;
|
|
45
|
+
productionCode: string;
|
|
46
|
+
assetId: string;
|
|
47
|
+
coachId: string;
|
|
48
|
+
sets: WorkoutSet[];
|
|
49
|
+
duration: number;
|
|
50
|
+
publishState: string;
|
|
51
|
+
programId: string | null;
|
|
52
|
+
level: string;
|
|
53
|
+
groupIds: string[];
|
|
54
|
+
targetArea: string;
|
|
55
|
+
tags: string[] | null;
|
|
56
|
+
bodyRegions: MuscleGroup[];
|
|
57
|
+
goalIds: string[] | null;
|
|
58
|
+
trainingEffectGoals: string[];
|
|
59
|
+
disableModification: boolean;
|
|
60
|
+
publishedAt: string;
|
|
61
|
+
localPublishedAt: string;
|
|
62
|
+
type: string;
|
|
63
|
+
userId: string;
|
|
64
|
+
style: string;
|
|
65
|
+
trainingType: string;
|
|
66
|
+
trainingTypeIds: string[] | null;
|
|
67
|
+
mobileFriendly: boolean;
|
|
68
|
+
live: boolean;
|
|
69
|
+
recoveryWeight: boolean;
|
|
70
|
+
supportedDevices: string[] | null;
|
|
71
|
+
featureGroupIds: string[] | null;
|
|
72
|
+
movementIds: string[];
|
|
73
|
+
muscleGroupsForExclusion: MuscleGroup[] | null;
|
|
74
|
+
playbackType: string;
|
|
75
|
+
isImported: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface WorkoutSet {
|
|
78
|
+
id: string;
|
|
79
|
+
workoutId: string;
|
|
80
|
+
blockStart: boolean;
|
|
81
|
+
movementId: string;
|
|
82
|
+
prescribedReps: number;
|
|
83
|
+
repetition: number;
|
|
84
|
+
repetitionTotal: number;
|
|
85
|
+
blockNumber: number;
|
|
86
|
+
burnout: boolean;
|
|
87
|
+
spotter: boolean;
|
|
88
|
+
eccentric: boolean;
|
|
89
|
+
chains: boolean;
|
|
90
|
+
skipSetup: boolean;
|
|
91
|
+
skipDemo: boolean;
|
|
92
|
+
finalSet: boolean;
|
|
93
|
+
calibration: boolean;
|
|
94
|
+
practice: boolean;
|
|
95
|
+
flex: boolean;
|
|
96
|
+
progressive: boolean;
|
|
97
|
+
weightPercentage: number;
|
|
98
|
+
warmUp: boolean;
|
|
99
|
+
durationBasedRepGoal: number;
|
|
100
|
+
setGroup: number;
|
|
101
|
+
round: number;
|
|
102
|
+
description: string;
|
|
103
|
+
dropSet: boolean;
|
|
104
|
+
omitempty: null | any;
|
|
105
|
+
}
|
|
106
|
+
export interface TonalSharedWorkout {
|
|
107
|
+
id: string;
|
|
108
|
+
sharerUserId: string;
|
|
109
|
+
parentWorkoutId: string;
|
|
110
|
+
workoutSnapshotId: string;
|
|
111
|
+
workoutSnapshotHash: string;
|
|
112
|
+
deepLinkUrl: string;
|
|
113
|
+
workoutSnapshot: TonalWorkout;
|
|
114
|
+
}
|
|
115
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dlwiest/ts-tonal-client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TypeScript client for Tonal API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "rollup -c --bundleConfigAsCjs"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/dlwiest/ts-tonal-client.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/dlwiest/ts-tonal-client/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/dlwiest/ts-tonal-client#readme",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@rollup/plugin-typescript": "^11.1.0",
|
|
28
|
+
"@types/node": "^20.14.8",
|
|
29
|
+
"rollup": "^3.20.2",
|
|
30
|
+
"rollup-plugin-dts": "^5.3.1",
|
|
31
|
+
"ts-node": "^10.9.2",
|
|
32
|
+
"tslib": "^2.7.0",
|
|
33
|
+
"typescript": "^5.5.4"
|
|
34
|
+
}
|
|
35
|
+
}
|