@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
|
@@ -0,0 +1,499 @@
|
|
|
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 axios from 'axios';
|
|
8
|
+
import { DuolingoAuthError, DuolingoCaptchaError, DuolingoClientError, DuolingoNotFoundError, } from './errors.js';
|
|
9
|
+
const USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36';
|
|
10
|
+
const BASE_URL = 'https://www.duolingo.com';
|
|
11
|
+
/**
|
|
12
|
+
* Fallback TTS CDN base URL used when the user data does not provide one.
|
|
13
|
+
* The actual URL is returned in the /users/<username> response as `tts_base_url`.
|
|
14
|
+
*/
|
|
15
|
+
const FALLBACK_TTS_BASE_URL = 'https://d7mj4aqfscim2.cloudfront.net/';
|
|
16
|
+
export class DuolingoClient {
|
|
17
|
+
http;
|
|
18
|
+
username;
|
|
19
|
+
jwt;
|
|
20
|
+
/** Cache of user data keyed by username. */
|
|
21
|
+
userDataCache = new Map();
|
|
22
|
+
/** Cache of v2 user data keyed by numeric user ID. */
|
|
23
|
+
userDataV2Cache = new Map();
|
|
24
|
+
/**
|
|
25
|
+
* Cached set of voice names discovered for each language via the session API.
|
|
26
|
+
* lang → Set<voiceName>
|
|
27
|
+
*/
|
|
28
|
+
voiceCache = new Map();
|
|
29
|
+
/** Voice URL dictionary: lang → word → Set<url> */
|
|
30
|
+
voiceUrlDict = new Map();
|
|
31
|
+
constructor(username, jwt) {
|
|
32
|
+
this.username = username;
|
|
33
|
+
this.jwt = jwt;
|
|
34
|
+
this.http = axios.create({
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${jwt}`,
|
|
37
|
+
'User-Agent': USER_AGENT,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Core data fetching
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
/**
|
|
46
|
+
* Fetch user data from /users/<username>.
|
|
47
|
+
* Results are cached per username for the lifetime of this client instance.
|
|
48
|
+
*/
|
|
49
|
+
async getUserData(username) {
|
|
50
|
+
const target = username ?? this.username;
|
|
51
|
+
const cached = this.userDataCache.get(target);
|
|
52
|
+
if (cached)
|
|
53
|
+
return cached;
|
|
54
|
+
const url = `${BASE_URL}/users/${encodeURIComponent(target)}`;
|
|
55
|
+
const resp = await this.makeRequest(url);
|
|
56
|
+
this.userDataCache.set(target, resp);
|
|
57
|
+
return resp;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Invalidate the user data cache for a specific username (or all).
|
|
61
|
+
*/
|
|
62
|
+
invalidateCache(username) {
|
|
63
|
+
if (username) {
|
|
64
|
+
this.userDataCache.delete(username);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.userDataCache.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetch daily XP progress data for a user.
|
|
72
|
+
* Uses the 2023-05-23 API which supersedes the 2017-06-30 endpoint.
|
|
73
|
+
*/
|
|
74
|
+
async getUserDataById(userId, fields) {
|
|
75
|
+
const ts = Date.now();
|
|
76
|
+
const fieldsParam = encodeURIComponent(fields.join(','));
|
|
77
|
+
const url = `${BASE_URL}/2023-05-23/users/${userId}?fields=${fieldsParam}&_=${ts}`;
|
|
78
|
+
return this.makeRequest(url);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the list of users the given user is following.
|
|
82
|
+
* Uses the 2023-05-23 API which supersedes the 2017-06-30 endpoint.
|
|
83
|
+
*
|
|
84
|
+
* The `viewerId` must be the authenticated user's ID (not the target user's ID).
|
|
85
|
+
*/
|
|
86
|
+
async getFollowing(userId) {
|
|
87
|
+
const ts = Date.now();
|
|
88
|
+
const viewerId = await this.getAuthenticatedUserId();
|
|
89
|
+
const url = `${BASE_URL}/2023-05-23/friends/users/${userId}/following?pageSize=500&viewerId=${viewerId}&_=${ts}`;
|
|
90
|
+
const resp = await this.makeRequest(url);
|
|
91
|
+
return resp.following.users;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get the list of users who follow the given user.
|
|
95
|
+
* Uses the 2023-05-23 API which supersedes the 2017-06-30 endpoint.
|
|
96
|
+
*
|
|
97
|
+
* The `viewerId` must be the authenticated user's ID (not the target user's ID).
|
|
98
|
+
*/
|
|
99
|
+
async getFollowers(userId) {
|
|
100
|
+
const ts = Date.now();
|
|
101
|
+
const viewerId = await this.getAuthenticatedUserId();
|
|
102
|
+
const url = `${BASE_URL}/2023-05-23/friends/users/${userId}/followers?pageSize=500&viewerId=${viewerId}&_=${ts}`;
|
|
103
|
+
const resp = await this.makeRequest(url);
|
|
104
|
+
return resp.followers.users;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a username to a numeric user ID using the 2023-05-23 API.
|
|
108
|
+
* Throws DuolingoNotFoundError if the username does not exist.
|
|
109
|
+
*/
|
|
110
|
+
async getUserIdByUsername(username) {
|
|
111
|
+
const ts = Date.now();
|
|
112
|
+
const url = `${BASE_URL}/2023-05-23/users?fields=users%7Bid%7D&username=${encodeURIComponent(username)}&_=${ts}`;
|
|
113
|
+
const resp = await this.makeRequest(url);
|
|
114
|
+
const id = resp.users[0]?.id;
|
|
115
|
+
if (id === undefined) {
|
|
116
|
+
throw new DuolingoNotFoundError(`User '${username}' not found.`);
|
|
117
|
+
}
|
|
118
|
+
return id;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Fetch rich user data from the 2023-05-23 API.
|
|
122
|
+
* Returns all courses including non-language subjects (math, chess, music),
|
|
123
|
+
* plus streak data, subscriber level, and more.
|
|
124
|
+
*
|
|
125
|
+
* Accepts either a numeric user ID or a username string.
|
|
126
|
+
* Results are cached per user ID for the lifetime of this client instance.
|
|
127
|
+
*/
|
|
128
|
+
async getUserDataV2(userIdOrUsername) {
|
|
129
|
+
// Resolve username to ID if needed
|
|
130
|
+
let userId;
|
|
131
|
+
if (typeof userIdOrUsername === 'string') {
|
|
132
|
+
// Check if it looks like a number
|
|
133
|
+
const parsed = parseInt(userIdOrUsername, 10);
|
|
134
|
+
if (!isNaN(parsed) && String(parsed) === userIdOrUsername) {
|
|
135
|
+
userId = parsed;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
userId = await this.getUserIdByUsername(userIdOrUsername);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
userId = userIdOrUsername;
|
|
143
|
+
}
|
|
144
|
+
const cached = this.userDataV2Cache.get(userId);
|
|
145
|
+
if (cached)
|
|
146
|
+
return cached;
|
|
147
|
+
const ts = Date.now();
|
|
148
|
+
const fields = [
|
|
149
|
+
'courses',
|
|
150
|
+
'creationDate',
|
|
151
|
+
'fromLanguage',
|
|
152
|
+
'hasPlus',
|
|
153
|
+
'id',
|
|
154
|
+
'learningLanguage',
|
|
155
|
+
'location',
|
|
156
|
+
'name',
|
|
157
|
+
'picture',
|
|
158
|
+
'streak',
|
|
159
|
+
'streakData{currentStreak,previousStreak,longestStreak,updatedTimestamp}',
|
|
160
|
+
'subscriberLevel',
|
|
161
|
+
'totalXp',
|
|
162
|
+
'username',
|
|
163
|
+
].join(',');
|
|
164
|
+
const url = `${BASE_URL}/2023-05-23/users/${userId}?fields=${encodeURIComponent(fields)}&_=${ts}`;
|
|
165
|
+
const resp = await this.makeRequest(url);
|
|
166
|
+
this.userDataV2Cache.set(userId, resp);
|
|
167
|
+
return resp;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get the full shop item catalogue from the 2023-05-23 API.
|
|
171
|
+
* Returns all purchasable items with prices, types, and last-used dates.
|
|
172
|
+
* This is a read-only endpoint — it does not purchase anything.
|
|
173
|
+
*/
|
|
174
|
+
async getShopItems() {
|
|
175
|
+
const ts = Date.now();
|
|
176
|
+
const url = `${BASE_URL}/2023-05-23/shop-items?_=${ts}`;
|
|
177
|
+
const resp = await this.makeRequest(url);
|
|
178
|
+
return resp.shopItems;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get the authenticated user's current hearts/health status.
|
|
182
|
+
* Returns heart count, max hearts, refill eligibility, and timing.
|
|
183
|
+
*/
|
|
184
|
+
async getHealth() {
|
|
185
|
+
const ts = Date.now();
|
|
186
|
+
const url = `${BASE_URL}/2023-05-23/users/${await this.getAuthenticatedUserId()}?fields=health&_=${ts}`;
|
|
187
|
+
const resp = await this.makeRequest(url);
|
|
188
|
+
return resp.health;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the authenticated user's gem and lingot balances.
|
|
192
|
+
*/
|
|
193
|
+
async getCurrencies() {
|
|
194
|
+
const ts = Date.now();
|
|
195
|
+
const url = `${BASE_URL}/2023-05-23/users/${await this.getAuthenticatedUserId()}?fields=gems,lingots&_=${ts}`;
|
|
196
|
+
const resp = await this.makeRequest(url);
|
|
197
|
+
return { gems: resp.gems, lingots: resp.lingots };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get the authenticated user's current streak goal.
|
|
201
|
+
*/
|
|
202
|
+
async getStreakGoalCurrent() {
|
|
203
|
+
const ts = Date.now();
|
|
204
|
+
const userId = await this.getAuthenticatedUserId();
|
|
205
|
+
const url = `${BASE_URL}/users/${userId}/streak-goal-current?_=${ts}`;
|
|
206
|
+
return this.makeRequest(url);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get the available next streak goal options for the authenticated user.
|
|
210
|
+
*/
|
|
211
|
+
async getStreakGoalNextOptions() {
|
|
212
|
+
const ts = Date.now();
|
|
213
|
+
const userId = await this.getAuthenticatedUserId();
|
|
214
|
+
const url = `${BASE_URL}/users/${userId}/streak-goal-next-options?_=${ts}`;
|
|
215
|
+
return this.makeRequest(url);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get the numeric user ID of the authenticated user.
|
|
219
|
+
* Cached via getUserData().
|
|
220
|
+
*/
|
|
221
|
+
async getAuthenticatedUserId() {
|
|
222
|
+
const userData = await this.getUserData();
|
|
223
|
+
return userData.id;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get leaderboard data for a time unit.
|
|
227
|
+
* Note: the /friendships/leaderboard_activity endpoint returns an empty ranking
|
|
228
|
+
* for most users. Prefer getFollowing() and sort by weeklyXp/monthlyXp instead.
|
|
229
|
+
*/
|
|
230
|
+
async getLeaderboard(unit, before) {
|
|
231
|
+
const url = `${BASE_URL}/friendships/leaderboard_activity?unit=${encodeURIComponent(unit)}&_=${encodeURIComponent(before)}`;
|
|
232
|
+
return this.makeRequest(url);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get the TTS base URL for the authenticated user.
|
|
236
|
+
* Falls back to the known CDN URL if not present in user data.
|
|
237
|
+
*/
|
|
238
|
+
async getTtsBaseUrl() {
|
|
239
|
+
const userData = await this.getUserData();
|
|
240
|
+
return this.normalizeTtsBaseUrl(userData.tts_base_url);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Discover available TTS voice names for a language by making a single
|
|
244
|
+
* GLOBAL_PRACTICE session request and extracting voice names from the
|
|
245
|
+
* TTS CDN URLs returned in the challenges.
|
|
246
|
+
*
|
|
247
|
+
* Voice names are extracted from URLs of the form:
|
|
248
|
+
* https://<cdn>/<voiceName>/<hash>
|
|
249
|
+
*
|
|
250
|
+
* Results are cached per language.
|
|
251
|
+
*/
|
|
252
|
+
async getLanguageVoices(langAbbr) {
|
|
253
|
+
const cached = this.voiceCache.get(langAbbr);
|
|
254
|
+
if (cached !== undefined) {
|
|
255
|
+
return [...cached];
|
|
256
|
+
}
|
|
257
|
+
const userData = await this.getUserData();
|
|
258
|
+
const langData = userData.language_data[langAbbr];
|
|
259
|
+
const fromLanguage = langData ? (langAbbr !== 'en' ? 'en' : 'de') : 'en';
|
|
260
|
+
const session = await this.getGlobalPracticeSession(langAbbr, fromLanguage);
|
|
261
|
+
const voices = new Set();
|
|
262
|
+
if (session) {
|
|
263
|
+
// Extract voice names from TTS URLs in challenges
|
|
264
|
+
for (const challenge of session.challenges) {
|
|
265
|
+
const voiceName = this.extractVoiceFromTtsUrl(challenge.tts);
|
|
266
|
+
if (voiceName)
|
|
267
|
+
voices.add(voiceName);
|
|
268
|
+
}
|
|
269
|
+
// Also extract from ttsAnnotations keys
|
|
270
|
+
for (const url of Object.keys(session.ttsAnnotations ?? {})) {
|
|
271
|
+
const voiceName = this.extractVoiceFromTtsUrl(url);
|
|
272
|
+
if (voiceName)
|
|
273
|
+
voices.add(voiceName);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
this.voiceCache.set(langAbbr, voices);
|
|
277
|
+
return [...voices];
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Build a TTS audio URL for a word using the tts_base_url from user data.
|
|
281
|
+
*
|
|
282
|
+
* URL format: {ttsBaseUrl}tts/{lang}/{voice}/token/{word}
|
|
283
|
+
* Without voice: {ttsBaseUrl}tts/{lang}/token/{word}
|
|
284
|
+
*/
|
|
285
|
+
async buildAudioUrl(word, langAbbr, voice) {
|
|
286
|
+
const ttsBaseUrl = await this.getTtsBaseUrl();
|
|
287
|
+
const base = ttsBaseUrl.endsWith('/') ? ttsBaseUrl : `${ttsBaseUrl}/`;
|
|
288
|
+
const encodedWord = encodeURIComponent(word);
|
|
289
|
+
if (voice) {
|
|
290
|
+
return `${base}tts/${langAbbr}/${voice}/token/${encodedWord}`;
|
|
291
|
+
}
|
|
292
|
+
return `${base}tts/${langAbbr}/token/${encodedWord}`;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Fetch a GLOBAL_PRACTICE session for a language.
|
|
296
|
+
* Used to discover TTS voice names and audio URLs.
|
|
297
|
+
*/
|
|
298
|
+
async getGlobalPracticeSession(langAbbr, fromLanguage) {
|
|
299
|
+
const url = `${BASE_URL}/2017-06-30/sessions`;
|
|
300
|
+
const data = {
|
|
301
|
+
fromLanguage,
|
|
302
|
+
learningLanguage: langAbbr,
|
|
303
|
+
challengeTypes: ['definition', 'translate'],
|
|
304
|
+
type: 'GLOBAL_PRACTICE',
|
|
305
|
+
juicy: true,
|
|
306
|
+
smartTipsVersion: 2,
|
|
307
|
+
};
|
|
308
|
+
let resp;
|
|
309
|
+
try {
|
|
310
|
+
resp = await this.http.post(url, data);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
if (axios.isAxiosError(err) && err.response) {
|
|
314
|
+
const status = err.response.status;
|
|
315
|
+
// Surface auth errors so callers know credentials are invalid
|
|
316
|
+
if (status === 401 || status === 403) {
|
|
317
|
+
throw new DuolingoAuthError('Authentication failed while starting a practice session.');
|
|
318
|
+
}
|
|
319
|
+
// Other HTTP errors (e.g. 404, 500) are non-fatal for voice discovery
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
if (resp.status !== 200)
|
|
325
|
+
return null;
|
|
326
|
+
return resp.data;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Fetch a practice session for a skill.
|
|
330
|
+
* Note: SKILL_PRACTICE is no longer supported by the API.
|
|
331
|
+
* Delegates to getGlobalPracticeSession instead.
|
|
332
|
+
*/
|
|
333
|
+
async getSession(_skillId, langAbbr) {
|
|
334
|
+
return this.getGlobalPracticeSession(langAbbr, langAbbr !== 'en' ? 'en' : 'de');
|
|
335
|
+
}
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Voice URL dictionary
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
/**
|
|
340
|
+
* Populate the voice URL dictionary for a language by scraping a session.
|
|
341
|
+
* Uses GLOBAL_PRACTICE (the only supported session type in the current API).
|
|
342
|
+
*/
|
|
343
|
+
async populateVoiceUrlDictionary(langAbbr) {
|
|
344
|
+
if (!this.voiceUrlDict.has(langAbbr)) {
|
|
345
|
+
this.voiceUrlDict.set(langAbbr, new Map());
|
|
346
|
+
}
|
|
347
|
+
// Safe: we just set it above if it wasn't present
|
|
348
|
+
const langDict = this.voiceUrlDict.get(langAbbr) ?? new Map();
|
|
349
|
+
this.voiceUrlDict.set(langAbbr, langDict);
|
|
350
|
+
const userData = await this.getUserData();
|
|
351
|
+
const langData = userData.language_data[langAbbr];
|
|
352
|
+
const fromLanguage = langData ? (langAbbr !== 'en' ? 'en' : 'de') : 'en';
|
|
353
|
+
const session = await this.getGlobalPracticeSession(langAbbr, fromLanguage);
|
|
354
|
+
if (!session)
|
|
355
|
+
return;
|
|
356
|
+
for (const challenge of session.challenges) {
|
|
357
|
+
if (challenge.prompt && challenge.tts) {
|
|
358
|
+
this.addToVoiceUrlDict(langDict, challenge.prompt, challenge.tts);
|
|
359
|
+
}
|
|
360
|
+
if (challenge.metadata?.non_character_tts?.tokens) {
|
|
361
|
+
for (const [word, url] of Object.entries(challenge.metadata.non_character_tts.tokens)) {
|
|
362
|
+
this.addToVoiceUrlDict(langDict, word, url);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (challenge.tokens) {
|
|
366
|
+
this.addTokenListToVoiceUrlDict(langDict, challenge.tokens);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get the voice URL dictionary for a language, populating it if needed.
|
|
372
|
+
*/
|
|
373
|
+
async getVoiceUrlDictionary(langAbbr) {
|
|
374
|
+
if (!this.voiceUrlDict.has(langAbbr)) {
|
|
375
|
+
await this.populateVoiceUrlDictionary(langAbbr);
|
|
376
|
+
}
|
|
377
|
+
return this.voiceUrlDict.get(langAbbr) ?? new Map();
|
|
378
|
+
}
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// Private helpers
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
/**
|
|
383
|
+
* Extract the voice name from a Duolingo TTS CDN URL.
|
|
384
|
+
* Supports two URL formats:
|
|
385
|
+
* Legacy: https://<cdn>/<voice>/<hash>
|
|
386
|
+
* Modern: https://<cdn>/tts/<lang>/<voice>/token/<word>
|
|
387
|
+
*/
|
|
388
|
+
extractVoiceFromTtsUrl(url) {
|
|
389
|
+
if (!url)
|
|
390
|
+
return null;
|
|
391
|
+
// Modern format: .../tts/<lang>/<voice>/token/<word>
|
|
392
|
+
const modern = /cloudfront\.net\/tts\/[^/]+\/([^/]+)\/token\//.exec(url);
|
|
393
|
+
if (modern)
|
|
394
|
+
return modern[1] ?? null;
|
|
395
|
+
// Legacy format: .../<voice>/<hash> — only applies to non-/tts/ URLs
|
|
396
|
+
if (url.includes('/tts/'))
|
|
397
|
+
return null;
|
|
398
|
+
const legacy = /cloudfront\.net\/([^/]+)\/[^/]+$/.exec(url);
|
|
399
|
+
return legacy?.[1] ?? null;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Normalize a TTS base URL to always end with a slash and use HTTPS.
|
|
403
|
+
*/
|
|
404
|
+
normalizeTtsBaseUrl(raw) {
|
|
405
|
+
if (!raw)
|
|
406
|
+
return FALLBACK_TTS_BASE_URL;
|
|
407
|
+
const url = raw.replace(/^http:\/\//, 'https://');
|
|
408
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
409
|
+
}
|
|
410
|
+
addToVoiceUrlDict(dict, word, url) {
|
|
411
|
+
const key = word.toLowerCase();
|
|
412
|
+
const existing = dict.get(key);
|
|
413
|
+
if (existing !== undefined) {
|
|
414
|
+
existing.add(url);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
dict.set(key, new Set([url]));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
addTokenListToVoiceUrlDict(dict, tokens) {
|
|
421
|
+
for (const token of tokens) {
|
|
422
|
+
if (Array.isArray(token)) {
|
|
423
|
+
this.addTokenListToVoiceUrlDict(dict, token);
|
|
424
|
+
}
|
|
425
|
+
else if (token !== null &&
|
|
426
|
+
typeof token === 'object' &&
|
|
427
|
+
'tts' in token &&
|
|
428
|
+
'value' in token) {
|
|
429
|
+
const t = token;
|
|
430
|
+
if (t.tts && t.value) {
|
|
431
|
+
this.addToVoiceUrlDict(dict, t.value, t.tts);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async makeRequest(url, data) {
|
|
437
|
+
let resp;
|
|
438
|
+
try {
|
|
439
|
+
if (data !== undefined) {
|
|
440
|
+
resp = await this.http.post(url, data);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
resp = await this.http.get(url);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
if (axios.isAxiosError(err) && err.response) {
|
|
448
|
+
const status = err.response.status;
|
|
449
|
+
const body = err.response.data;
|
|
450
|
+
if (status === 403 && body.blockScript != null) {
|
|
451
|
+
throw new DuolingoCaptchaError();
|
|
452
|
+
}
|
|
453
|
+
if (status === 401 || status === 403) {
|
|
454
|
+
throw new DuolingoAuthError('Authentication failed. Your JWT token may have expired. ' +
|
|
455
|
+
'Extract a new one from your browser: ' +
|
|
456
|
+
"document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11)");
|
|
457
|
+
}
|
|
458
|
+
if (status === 404) {
|
|
459
|
+
throw new DuolingoNotFoundError(`Resource not found: ${url}`);
|
|
460
|
+
}
|
|
461
|
+
throw new DuolingoClientError(`Duolingo API error ${status}: ${JSON.stringify(body)}`);
|
|
462
|
+
}
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
return resp.data;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Singleton factory
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
let _client = null;
|
|
472
|
+
/**
|
|
473
|
+
* Get or create the singleton DuolingoClient.
|
|
474
|
+
* Reads DUOLINGO_USERNAME and DUOLINGO_JWT from environment variables.
|
|
475
|
+
*/
|
|
476
|
+
export function getClient() {
|
|
477
|
+
if (_client)
|
|
478
|
+
return _client;
|
|
479
|
+
const username = process.env.DUOLINGO_USERNAME;
|
|
480
|
+
const jwt = process.env.DUOLINGO_JWT;
|
|
481
|
+
if (!username) {
|
|
482
|
+
throw new DuolingoAuthError('DUOLINGO_USERNAME environment variable is not set. ' +
|
|
483
|
+
'Please set it to your Duolingo username.');
|
|
484
|
+
}
|
|
485
|
+
if (!jwt) {
|
|
486
|
+
throw new DuolingoAuthError('DUOLINGO_JWT environment variable is not set. ' +
|
|
487
|
+
'Extract your JWT token from the browser console: ' +
|
|
488
|
+
"document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11)");
|
|
489
|
+
}
|
|
490
|
+
_client = new DuolingoClient(username, jwt);
|
|
491
|
+
return _client;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Reset the singleton client (useful for testing or credential rotation).
|
|
495
|
+
*/
|
|
496
|
+
export function resetClient() {
|
|
497
|
+
_client = null;
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=duolingo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duolingo.js","sourceRoot":"","sources":["../../src/client/duolingo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for the Duolingo API client.
|
|
3
|
+
*/
|
|
4
|
+
/** Base error for all Duolingo client failures. */
|
|
5
|
+
export declare class DuolingoClientError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/** Raised when credentials are missing or invalid. */
|
|
9
|
+
export declare class DuolingoAuthError extends DuolingoClientError {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/** Raised when a requested user or resource is not found. */
|
|
13
|
+
export declare class DuolingoNotFoundError extends DuolingoClientError {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
/** Raised when Duolingo returns a CAPTCHA challenge. */
|
|
17
|
+
export declare class DuolingoCaptchaError extends DuolingoClientError {
|
|
18
|
+
constructor();
|
|
19
|
+
}
|
|
20
|
+
/** Raised when the language is not in the user's learning list. */
|
|
21
|
+
export declare class DuolingoLanguageNotFoundError extends DuolingoClientError {
|
|
22
|
+
constructor(lang: string);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/client/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,mDAAmD;AACnD,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,sDAAsD;AACtD,qBAAa,iBAAkB,SAAQ,mBAAmB;gBAC5C,OAAO,EAAE,MAAM;CAI5B;AAED,6DAA6D;AAC7D,qBAAa,qBAAsB,SAAQ,mBAAmB;gBAChD,OAAO,EAAE,MAAM;CAI5B;AAED,wDAAwD;AACxD,qBAAa,oBAAqB,SAAQ,mBAAmB;;CAQ5D;AAED,mEAAmE;AACnE,qBAAa,6BAA8B,SAAQ,mBAAmB;gBACxD,IAAI,EAAE,MAAM;CAOzB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes for the Duolingo API client.
|
|
3
|
+
*/
|
|
4
|
+
/** Base error for all Duolingo client failures. */
|
|
5
|
+
export class DuolingoClientError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'DuolingoClientError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Raised when credentials are missing or invalid. */
|
|
12
|
+
export class DuolingoAuthError extends DuolingoClientError {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'DuolingoAuthError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Raised when a requested user or resource is not found. */
|
|
19
|
+
export class DuolingoNotFoundError extends DuolingoClientError {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'DuolingoNotFoundError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Raised when Duolingo returns a CAPTCHA challenge. */
|
|
26
|
+
export class DuolingoCaptchaError extends DuolingoClientError {
|
|
27
|
+
constructor() {
|
|
28
|
+
super('Request was blocked by Duolingo CAPTCHA. ' +
|
|
29
|
+
'Try refreshing your JWT token by logging in again at duolingo.com.');
|
|
30
|
+
this.name = 'DuolingoCaptchaError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Raised when the language is not in the user's learning list. */
|
|
34
|
+
export class DuolingoLanguageNotFoundError extends DuolingoClientError {
|
|
35
|
+
constructor(lang) {
|
|
36
|
+
super(`Language '${lang}' not found in user's language data. ` +
|
|
37
|
+
'Make sure the user is learning this language.');
|
|
38
|
+
this.name = 'DuolingoLanguageNotFoundError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/client/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,mDAAmD;AACnD,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,sDAAsD;AACtD,MAAM,OAAO,iBAAkB,SAAQ,mBAAmB;IACxD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,6DAA6D;AAC7D,MAAM,OAAO,qBAAsB,SAAQ,mBAAmB;IAC5D,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,wDAAwD;AACxD,MAAM,OAAO,oBAAqB,SAAQ,mBAAmB;IAC3D;QACE,KAAK,CACH,2CAA2C;YACzC,oEAAoE,CACvE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,mEAAmE;AACnE,MAAM,OAAO,6BAA8B,SAAQ,mBAAmB;IACpE,YAAY,IAAY;QACtB,KAAK,CACH,aAAa,IAAI,uCAAuC;YACtD,+CAA+C,CAClD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAC9C,CAAC;CACF"}
|