@udondan/duolingo 1.0.0
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 +373 -0
- package/dist/client/duolingo.d.ts +170 -0
- package/dist/client/duolingo.d.ts.map +1 -0
- package/dist/client/duolingo.js +499 -0
- package/dist/client/duolingo.js.map +1 -0
- package/dist/client/errors.d.ts +24 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +41 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/types.d.ts +272 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +6 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +40 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/account.d.ts +10 -0
- package/dist/tools/account.d.ts.map +1 -0
- package/dist/tools/account.js +746 -0
- package/dist/tools/account.js.map +1 -0
- package/dist/tools/helpers.d.ts +18 -0
- package/dist/tools/helpers.d.ts.map +1 -0
- package/dist/tools/helpers.js +83 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/language.d.ts +11 -0
- package/dist/tools/language.d.ts.map +1 -0
- package/dist/tools/language.js +611 -0
- package/dist/tools/language.js.map +1 -0
- package/dist/tools/shop.d.ts +8 -0
- package/dist/tools/shop.d.ts.map +1 -0
- package/dist/tools/shop.js +125 -0
- package/dist/tools/shop.js.map +1 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Schroeder
|
|
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,373 @@
|
|
|
1
|
+
# @udondan/duolingo
|
|
2
|
+
|
|
3
|
+
A TypeScript package that provides both a **Duolingo API client library** and an
|
|
4
|
+
**[MCP](https://modelcontextprotocol.io) server** for LLM agents (e.g. Claude).
|
|
5
|
+
|
|
6
|
+
Built natively in TypeScript against the unofficial Duolingo REST API — no third-party
|
|
7
|
+
Duolingo library dependency.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Getting Your JWT Token](#getting-your-jwt-token)
|
|
12
|
+
- [MCP Server](#mcp-server)
|
|
13
|
+
- [Installation](#mcp-installation)
|
|
14
|
+
- [Claude Desktop](#claude-desktop)
|
|
15
|
+
- [Claude Code](#claude-code)
|
|
16
|
+
- [Available Tools](#available-tools)
|
|
17
|
+
- [Library](#library)
|
|
18
|
+
- [Installation](#library-installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [API Reference](#api-reference)
|
|
21
|
+
- [Development](#development)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Getting Your JWT Token
|
|
26
|
+
|
|
27
|
+
Both the MCP server and the library require a Duolingo JWT token for authentication.
|
|
28
|
+
|
|
29
|
+
1. Log in to [duolingo.com](https://www.duolingo.com) in your browser.
|
|
30
|
+
2. Open the browser developer console (F12 → Console tab).
|
|
31
|
+
3. Run:
|
|
32
|
+
```js
|
|
33
|
+
document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11)
|
|
34
|
+
```
|
|
35
|
+
4. Copy the output — that is your JWT token.
|
|
36
|
+
|
|
37
|
+
> **Note**: JWT tokens expire. If you get authentication errors, repeat the steps above.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## MCP Server
|
|
42
|
+
|
|
43
|
+
### MCP Installation
|
|
44
|
+
|
|
45
|
+
**Option A — run directly with npx (no install needed):**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
DUOLINGO_USERNAME=your_username DUOLINGO_JWT=your_jwt npx @udondan/duolingo
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Option B — install globally:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g @udondan/duolingo
|
|
55
|
+
DUOLINGO_USERNAME=your_username DUOLINGO_JWT=your_jwt duolingo-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Option C — clone and build from source:**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/udondan/duolingo.git
|
|
62
|
+
cd duolingo
|
|
63
|
+
npm install && npm run build
|
|
64
|
+
node dist/server.js
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Claude Desktop
|
|
68
|
+
|
|
69
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"duolingo": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["-y", "@udondan/duolingo"],
|
|
77
|
+
"env": {
|
|
78
|
+
"DUOLINGO_USERNAME": "your_username",
|
|
79
|
+
"DUOLINGO_JWT": "your_jwt_token"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Claude Code
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
claude mcp add duolingo -- npx -y @udondan/duolingo
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then set the environment variables before starting Claude Code:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export DUOLINGO_USERNAME="your_username"
|
|
96
|
+
export DUOLINGO_JWT="your_jwt_token"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Available Tools
|
|
100
|
+
|
|
101
|
+
All tools are **read-only** — the server never modifies your Duolingo account.
|
|
102
|
+
|
|
103
|
+
#### Account
|
|
104
|
+
|
|
105
|
+
| Tool | Description |
|
|
106
|
+
|------|-------------|
|
|
107
|
+
| `duolingo_get_user_info` | Profile: username, name, bio, location, avatar, followers, learning language |
|
|
108
|
+
| `duolingo_get_settings` | Notification and account settings |
|
|
109
|
+
| `duolingo_get_streak_info` | Current streak, longest streak, daily goal, extended today |
|
|
110
|
+
| `duolingo_get_daily_xp_progress` | XP goal, XP earned today, lessons completed today |
|
|
111
|
+
| `duolingo_get_languages` | Languages being learned (full names or abbreviations) |
|
|
112
|
+
| `duolingo_get_courses` | All courses including Math, Chess, and Music with XP per course |
|
|
113
|
+
| `duolingo_get_friends` | Users the authenticated user follows, with total XP |
|
|
114
|
+
| `duolingo_get_calendar` | Recent activity calendar for the current course (~last 2 weeks) |
|
|
115
|
+
| `duolingo_get_leaderboard` | Authenticated user's friends sorted by XP for week or month |
|
|
116
|
+
| `duolingo_get_shop_items` | Full shop catalogue with prices and item types |
|
|
117
|
+
| `duolingo_get_health` | Current hearts count, max hearts, refill timing |
|
|
118
|
+
| `duolingo_get_currencies` | Gem and lingot balances |
|
|
119
|
+
| `duolingo_get_streak_goal` | Current streak goal with upcoming checkpoints |
|
|
120
|
+
|
|
121
|
+
#### Language
|
|
122
|
+
|
|
123
|
+
| Tool | Description |
|
|
124
|
+
|------|-------------|
|
|
125
|
+
| `duolingo_get_language_details` | Level, points, streak for a specific language |
|
|
126
|
+
| `duolingo_get_language_progress` | Detailed progress: level %, points to next level, fluency |
|
|
127
|
+
| `duolingo_get_known_topics` | Learned topic/skill names |
|
|
128
|
+
| `duolingo_get_unknown_topics` | Not-yet-learned topics |
|
|
129
|
+
| `duolingo_get_golden_topics` | Fully mastered topics (strength = 1.0) |
|
|
130
|
+
| `duolingo_get_reviewable_topics` | Learned but not fully mastered topics |
|
|
131
|
+
| `duolingo_get_known_words` | Set of known words for a language |
|
|
132
|
+
| `duolingo_get_learned_skills` | Full skill objects sorted by learning order |
|
|
133
|
+
| `duolingo_get_language_voices` | Available TTS voice names for a language |
|
|
134
|
+
| `duolingo_get_audio_url` | Pronunciation audio URL for a word |
|
|
135
|
+
|
|
136
|
+
#### Utilities
|
|
137
|
+
|
|
138
|
+
| Tool | Description |
|
|
139
|
+
|------|-------------|
|
|
140
|
+
| `duolingo_get_language_from_abbr` | Convert language abbreviation to full name (e.g. `fr` → `French`) |
|
|
141
|
+
| `duolingo_get_abbreviation_of` | Convert full language name to abbreviation (e.g. `French` → `fr`) |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Library
|
|
146
|
+
|
|
147
|
+
### Library Installation
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm install @udondan/duolingo
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Quick Start
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { DuolingoClient } from '@udondan/duolingo';
|
|
157
|
+
|
|
158
|
+
const client = new DuolingoClient('your_username', 'your_jwt_token');
|
|
159
|
+
|
|
160
|
+
// Get all courses including Math, Chess, and Music
|
|
161
|
+
const userData = await client.getUserData();
|
|
162
|
+
const userId = userData.id;
|
|
163
|
+
const v2 = await client.getUserDataV2(userId);
|
|
164
|
+
for (const course of v2.courses) {
|
|
165
|
+
console.log(`${course.subject}: ${course.xp} XP`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get streak info
|
|
169
|
+
const streak = v2.streak;
|
|
170
|
+
const longestStreak = v2.streakData.longestStreak?.length;
|
|
171
|
+
|
|
172
|
+
// Get friends
|
|
173
|
+
const friends = await client.getFollowing(userId);
|
|
174
|
+
for (const friend of friends) {
|
|
175
|
+
console.log(`${friend.username}: ${friend.totalXp} XP`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get shop items
|
|
179
|
+
const items = await client.getShopItems();
|
|
180
|
+
const streakFreeze = items.find(i => i.id === 'streak_freeze');
|
|
181
|
+
|
|
182
|
+
// Get hearts
|
|
183
|
+
const health = await client.getHealth();
|
|
184
|
+
console.log(`Hearts: ${health.hearts}/${health.maxHearts}`);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### API Reference
|
|
188
|
+
|
|
189
|
+
#### `new DuolingoClient(username, jwt)`
|
|
190
|
+
|
|
191
|
+
Creates a new client instance. Results are cached per instance.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const client = new DuolingoClient('your_username', 'your_jwt_token');
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### User Data
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// Legacy API — returns language_data with skills, calendar, etc.
|
|
201
|
+
// Required for: language details, topics, known words, learned skills
|
|
202
|
+
const userData = await client.getUserData(username?);
|
|
203
|
+
|
|
204
|
+
// 2023-05-23 API — returns all courses (language + math/chess/music),
|
|
205
|
+
// richer streak data, subscriber level, gems, health
|
|
206
|
+
const userId = userData.id;
|
|
207
|
+
const v2 = await client.getUserDataV2(userId);
|
|
208
|
+
|
|
209
|
+
// Resolve a username to a numeric user ID
|
|
210
|
+
const id = await client.getUserIdByUsername('someuser');
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Courses (all subjects)
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const v2 = await client.getUserDataV2(userId);
|
|
217
|
+
|
|
218
|
+
// All courses
|
|
219
|
+
v2.courses; // DuolingoCourse[]
|
|
220
|
+
|
|
221
|
+
// Filter by subject
|
|
222
|
+
const langCourses = v2.courses.filter(c => c.subject === 'language');
|
|
223
|
+
const mathCourses = v2.courses.filter(c => c.subject === 'math');
|
|
224
|
+
const chessCourses = v2.courses.filter(c => c.subject === 'chess');
|
|
225
|
+
const musicCourses = v2.courses.filter(c => c.subject === 'music');
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Each course has: `id`, `subject`, `topic`, `xp`, `fromLanguage`.
|
|
229
|
+
Language courses also have: `learningLanguage`, `title`, `authorId`.
|
|
230
|
+
|
|
231
|
+
#### Daily XP Progress
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const progress = await client.getUserDataById(userId, ['xpGoal', 'xpGains', 'streakData']);
|
|
235
|
+
// progress.xpGoal — daily XP goal
|
|
236
|
+
// progress.xpGains — array of { xp, skillId, time } for recent lessons
|
|
237
|
+
// progress.streakData.updatedTimestamp — last streak update
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### Friends & Social
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// People this user follows (= friends)
|
|
244
|
+
const following = await client.getFollowing(userId);
|
|
245
|
+
// following[0].username, .totalXp, .userScore?.score (weekly XP), .isFollowedBy
|
|
246
|
+
|
|
247
|
+
// People who follow this user
|
|
248
|
+
const followers = await client.getFollowers(userId);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Shop, Health & Currencies
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Full shop catalogue (read-only)
|
|
255
|
+
const items = await client.getShopItems();
|
|
256
|
+
// items[0].id, .name, .type, .price, .currencyType, .lastUsedDate
|
|
257
|
+
|
|
258
|
+
// Hearts / health (authenticated user only)
|
|
259
|
+
const health = await client.getHealth();
|
|
260
|
+
// health.hearts, .maxHearts, .eligibleForFreeRefill, .secondsUntilNextHeartSegment
|
|
261
|
+
|
|
262
|
+
// Gem and lingot balances (authenticated user only)
|
|
263
|
+
const { gems, lingots } = await client.getCurrencies();
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Streak Goals
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// Current streak goal with checkpoints
|
|
270
|
+
const goal = await client.getStreakGoalCurrent();
|
|
271
|
+
// goal.hasActiveGoal, goal.streakGoal.lastCompleteGoal, .checkpoints, .nextSelectedGoal
|
|
272
|
+
|
|
273
|
+
// Available next goal options
|
|
274
|
+
const options = await client.getStreakGoalNextOptions();
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### TTS Audio
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Discover available voices for a language
|
|
281
|
+
const voices = await client.getLanguageVoices('es'); // ['beaes', 'juniores', ...]
|
|
282
|
+
|
|
283
|
+
// Build an audio URL
|
|
284
|
+
const url = await client.buildAudioUrl('hola', 'es');
|
|
285
|
+
const urlWithVoice = await client.buildAudioUrl('hola', 'es', 'beaes');
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
#### Error Handling
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import {
|
|
292
|
+
DuolingoClient,
|
|
293
|
+
DuolingoAuthError,
|
|
294
|
+
DuolingoNotFoundError,
|
|
295
|
+
DuolingoCaptchaError,
|
|
296
|
+
DuolingoClientError,
|
|
297
|
+
} from '@udondan/duolingo';
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const data = await client.getUserData('someuser');
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (err instanceof DuolingoAuthError) {
|
|
303
|
+
// JWT expired — extract a new one from the browser
|
|
304
|
+
} else if (err instanceof DuolingoNotFoundError) {
|
|
305
|
+
// User does not exist
|
|
306
|
+
} else if (err instanceof DuolingoCaptchaError) {
|
|
307
|
+
// Duolingo blocked the request — try again later
|
|
308
|
+
} else if (err instanceof DuolingoClientError) {
|
|
309
|
+
// Other API error
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Development
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
git clone https://github.com/udondan/duolingo.git
|
|
320
|
+
cd duolingo
|
|
321
|
+
npm install
|
|
322
|
+
|
|
323
|
+
# Build
|
|
324
|
+
npm run build
|
|
325
|
+
|
|
326
|
+
# Run tests (unit + integration)
|
|
327
|
+
npm test
|
|
328
|
+
|
|
329
|
+
# Integration tests require credentials:
|
|
330
|
+
export DUOLINGO_USERNAME="your_username"
|
|
331
|
+
export DUOLINGO_JWT="your_jwt_token"
|
|
332
|
+
npm test
|
|
333
|
+
|
|
334
|
+
# Type-check without building
|
|
335
|
+
npm run typecheck
|
|
336
|
+
|
|
337
|
+
# Run MCP server in dev mode (no build step)
|
|
338
|
+
npm run dev
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Project Structure
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
src/
|
|
345
|
+
├── index.ts # Library entry point — exports client, types, errors
|
|
346
|
+
├── server.ts # MCP server entry point (stdio transport)
|
|
347
|
+
├── client/
|
|
348
|
+
│ ├── duolingo.ts # DuolingoClient — all API methods
|
|
349
|
+
│ ├── types.ts # TypeScript interfaces for all API responses
|
|
350
|
+
│ └── errors.ts # Custom error classes
|
|
351
|
+
└── tools/
|
|
352
|
+
├── account.ts # Account tools (13)
|
|
353
|
+
├── language.ts # Language tools (11)
|
|
354
|
+
├── shop.ts # Utility tools (2)
|
|
355
|
+
└── helpers.ts # Shared utilities (error handling, Zod schemas)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### API Versions Used
|
|
359
|
+
|
|
360
|
+
| Endpoint | API Version | Used For |
|
|
361
|
+
|----------|-------------|----------|
|
|
362
|
+
| `/users/<username>` | Legacy | User profile, language data, skills, calendar |
|
|
363
|
+
| `/2023-05-23/users/{id}` | Current | Courses (all subjects), streak, health, gems |
|
|
364
|
+
| `/2023-05-23/friends/users/{id}/following` | Current | Friends, leaderboard |
|
|
365
|
+
| `/2023-05-23/shop-items` | Current | Shop catalogue |
|
|
366
|
+
| `/users/{id}/streak-goal-current` | Current | Streak goals |
|
|
367
|
+
| `/2017-06-30/sessions` | Current | TTS voice discovery |
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native TypeScript Duolingo API client.
|
|
3
|
+
*
|
|
4
|
+
* Calls the unofficial Duolingo REST API directly using axios.
|
|
5
|
+
* No third-party Duolingo library dependency.
|
|
6
|
+
*/
|
|
7
|
+
import type { DuolingoUserData, DuolingoDailyProgress, DuolingoLeaderboardData, DuolingoFriendUser, DuolingoUserDataV2, DuolingoShopItem, DuolingoHealth, DuolingoStreakGoalCurrentResponse, DuolingoStreakGoalNextOptionsResponse, DuolingoSessionResponse } from './types.js';
|
|
8
|
+
export declare class DuolingoClient {
|
|
9
|
+
private readonly http;
|
|
10
|
+
private readonly username;
|
|
11
|
+
private readonly jwt;
|
|
12
|
+
/** Cache of user data keyed by username. */
|
|
13
|
+
private readonly userDataCache;
|
|
14
|
+
/** Cache of v2 user data keyed by numeric user ID. */
|
|
15
|
+
private readonly userDataV2Cache;
|
|
16
|
+
/**
|
|
17
|
+
* Cached set of voice names discovered for each language via the session API.
|
|
18
|
+
* lang → Set<voiceName>
|
|
19
|
+
*/
|
|
20
|
+
private voiceCache;
|
|
21
|
+
/** Voice URL dictionary: lang → word → Set<url> */
|
|
22
|
+
private voiceUrlDict;
|
|
23
|
+
constructor(username: string, jwt: string);
|
|
24
|
+
/**
|
|
25
|
+
* Fetch user data from /users/<username>.
|
|
26
|
+
* Results are cached per username for the lifetime of this client instance.
|
|
27
|
+
*/
|
|
28
|
+
getUserData(username?: string): Promise<DuolingoUserData>;
|
|
29
|
+
/**
|
|
30
|
+
* Invalidate the user data cache for a specific username (or all).
|
|
31
|
+
*/
|
|
32
|
+
invalidateCache(username?: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch daily XP progress data for a user.
|
|
35
|
+
* Uses the 2023-05-23 API which supersedes the 2017-06-30 endpoint.
|
|
36
|
+
*/
|
|
37
|
+
getUserDataById(userId: number, fields: string[]): Promise<DuolingoDailyProgress>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the list of users the given user is following.
|
|
40
|
+
* Uses the 2023-05-23 API which supersedes the 2017-06-30 endpoint.
|
|
41
|
+
*
|
|
42
|
+
* The `viewerId` must be the authenticated user's ID (not the target user's ID).
|
|
43
|
+
*/
|
|
44
|
+
getFollowing(userId: number): Promise<DuolingoFriendUser[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Get the list of users who follow the given user.
|
|
47
|
+
* Uses the 2023-05-23 API which supersedes the 2017-06-30 endpoint.
|
|
48
|
+
*
|
|
49
|
+
* The `viewerId` must be the authenticated user's ID (not the target user's ID).
|
|
50
|
+
*/
|
|
51
|
+
getFollowers(userId: number): Promise<DuolingoFriendUser[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a username to a numeric user ID using the 2023-05-23 API.
|
|
54
|
+
* Throws DuolingoNotFoundError if the username does not exist.
|
|
55
|
+
*/
|
|
56
|
+
getUserIdByUsername(username: string): Promise<number>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch rich user data from the 2023-05-23 API.
|
|
59
|
+
* Returns all courses including non-language subjects (math, chess, music),
|
|
60
|
+
* plus streak data, subscriber level, and more.
|
|
61
|
+
*
|
|
62
|
+
* Accepts either a numeric user ID or a username string.
|
|
63
|
+
* Results are cached per user ID for the lifetime of this client instance.
|
|
64
|
+
*/
|
|
65
|
+
getUserDataV2(userIdOrUsername: number | string): Promise<DuolingoUserDataV2>;
|
|
66
|
+
/**
|
|
67
|
+
* Get the full shop item catalogue from the 2023-05-23 API.
|
|
68
|
+
* Returns all purchasable items with prices, types, and last-used dates.
|
|
69
|
+
* This is a read-only endpoint — it does not purchase anything.
|
|
70
|
+
*/
|
|
71
|
+
getShopItems(): Promise<DuolingoShopItem[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Get the authenticated user's current hearts/health status.
|
|
74
|
+
* Returns heart count, max hearts, refill eligibility, and timing.
|
|
75
|
+
*/
|
|
76
|
+
getHealth(): Promise<DuolingoHealth>;
|
|
77
|
+
/**
|
|
78
|
+
* Get the authenticated user's gem and lingot balances.
|
|
79
|
+
*/
|
|
80
|
+
getCurrencies(): Promise<{
|
|
81
|
+
gems: number;
|
|
82
|
+
lingots: number;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Get the authenticated user's current streak goal.
|
|
86
|
+
*/
|
|
87
|
+
getStreakGoalCurrent(): Promise<DuolingoStreakGoalCurrentResponse>;
|
|
88
|
+
/**
|
|
89
|
+
* Get the available next streak goal options for the authenticated user.
|
|
90
|
+
*/
|
|
91
|
+
getStreakGoalNextOptions(): Promise<DuolingoStreakGoalNextOptionsResponse>;
|
|
92
|
+
/**
|
|
93
|
+
* Get the numeric user ID of the authenticated user.
|
|
94
|
+
* Cached via getUserData().
|
|
95
|
+
*/
|
|
96
|
+
private getAuthenticatedUserId;
|
|
97
|
+
/**
|
|
98
|
+
* Get leaderboard data for a time unit.
|
|
99
|
+
* Note: the /friendships/leaderboard_activity endpoint returns an empty ranking
|
|
100
|
+
* for most users. Prefer getFollowing() and sort by weeklyXp/monthlyXp instead.
|
|
101
|
+
*/
|
|
102
|
+
getLeaderboard(unit: string, before: string): Promise<DuolingoLeaderboardData>;
|
|
103
|
+
/**
|
|
104
|
+
* Get the TTS base URL for the authenticated user.
|
|
105
|
+
* Falls back to the known CDN URL if not present in user data.
|
|
106
|
+
*/
|
|
107
|
+
getTtsBaseUrl(): Promise<string>;
|
|
108
|
+
/**
|
|
109
|
+
* Discover available TTS voice names for a language by making a single
|
|
110
|
+
* GLOBAL_PRACTICE session request and extracting voice names from the
|
|
111
|
+
* TTS CDN URLs returned in the challenges.
|
|
112
|
+
*
|
|
113
|
+
* Voice names are extracted from URLs of the form:
|
|
114
|
+
* https://<cdn>/<voiceName>/<hash>
|
|
115
|
+
*
|
|
116
|
+
* Results are cached per language.
|
|
117
|
+
*/
|
|
118
|
+
getLanguageVoices(langAbbr: string): Promise<string[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Build a TTS audio URL for a word using the tts_base_url from user data.
|
|
121
|
+
*
|
|
122
|
+
* URL format: {ttsBaseUrl}tts/{lang}/{voice}/token/{word}
|
|
123
|
+
* Without voice: {ttsBaseUrl}tts/{lang}/token/{word}
|
|
124
|
+
*/
|
|
125
|
+
buildAudioUrl(word: string, langAbbr: string, voice?: string): Promise<string>;
|
|
126
|
+
/**
|
|
127
|
+
* Fetch a GLOBAL_PRACTICE session for a language.
|
|
128
|
+
* Used to discover TTS voice names and audio URLs.
|
|
129
|
+
*/
|
|
130
|
+
getGlobalPracticeSession(langAbbr: string, fromLanguage: string): Promise<DuolingoSessionResponse | null>;
|
|
131
|
+
/**
|
|
132
|
+
* Fetch a practice session for a skill.
|
|
133
|
+
* Note: SKILL_PRACTICE is no longer supported by the API.
|
|
134
|
+
* Delegates to getGlobalPracticeSession instead.
|
|
135
|
+
*/
|
|
136
|
+
getSession(_skillId: string, langAbbr: string): Promise<DuolingoSessionResponse | null>;
|
|
137
|
+
/**
|
|
138
|
+
* Populate the voice URL dictionary for a language by scraping a session.
|
|
139
|
+
* Uses GLOBAL_PRACTICE (the only supported session type in the current API).
|
|
140
|
+
*/
|
|
141
|
+
populateVoiceUrlDictionary(langAbbr: string): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Get the voice URL dictionary for a language, populating it if needed.
|
|
144
|
+
*/
|
|
145
|
+
getVoiceUrlDictionary(langAbbr: string): Promise<Map<string, Set<string>>>;
|
|
146
|
+
/**
|
|
147
|
+
* Extract the voice name from a Duolingo TTS CDN URL.
|
|
148
|
+
* Supports two URL formats:
|
|
149
|
+
* Legacy: https://<cdn>/<voice>/<hash>
|
|
150
|
+
* Modern: https://<cdn>/tts/<lang>/<voice>/token/<word>
|
|
151
|
+
*/
|
|
152
|
+
private extractVoiceFromTtsUrl;
|
|
153
|
+
/**
|
|
154
|
+
* Normalize a TTS base URL to always end with a slash and use HTTPS.
|
|
155
|
+
*/
|
|
156
|
+
private normalizeTtsBaseUrl;
|
|
157
|
+
private addToVoiceUrlDict;
|
|
158
|
+
private addTokenListToVoiceUrlDict;
|
|
159
|
+
private makeRequest;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get or create the singleton DuolingoClient.
|
|
163
|
+
* Reads DUOLINGO_USERNAME and DUOLINGO_JWT from environment variables.
|
|
164
|
+
*/
|
|
165
|
+
export declare function getClient(): DuolingoClient;
|
|
166
|
+
/**
|
|
167
|
+
* Reset the singleton client (useful for testing or credential rotation).
|
|
168
|
+
*/
|
|
169
|
+
export declare function resetClient(): void;
|
|
170
|
+
//# sourceMappingURL=duolingo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duolingo.d.ts","sourceRoot":"","sources":["../../src/client/duolingo.ts"],"names":[],"mappings":"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"}
|