forked from KartikTalwar/Duolingo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathduolingo.py
522 lines (428 loc) · 18.5 KB
/
duolingo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
"""Unofficial API for duolingo.com"""
import re
import json
import random
import requests
from werkzeug.datastructures import MultiDict
__version__ = "0.3"
__author__ = "Kartik Talwar"
__url__ = "https://github.com/KartikTalwar/duolingo"
class Struct:
def __init__(self, **entries):
self.__dict__.update(entries)
class AlreadyHaveStoreItemException(Exception):
pass
class Duolingo(object):
def __init__(self, username, password=None):
self.username = username
self.password = password
self.user_url = "https://duolingo.com/users/%s" % self.username
self.session = requests.Session()
self.leader_data = None
self.jwt = None
if password:
self._login()
self.user_data = Struct(**self._get_data())
def _make_req(self, url, data=None):
headers = {}
if self.jwt is not None:
headers['Authorization'] = 'Bearer ' + self.jwt
req = requests.Request('POST' if data else 'GET',
url,
json=data,
headers=headers,
cookies=self.session.cookies)
prepped = req.prepare()
return self.session.send(prepped)
def _login(self):
"""
Authenticate through ``https://www.duolingo.com/login``.
"""
login_url = "https://www.duolingo.com/login"
data = {"login": self.username, "password": self.password}
request = self._make_req(login_url, data)
attempt = request.json()
if attempt.get('response') == 'OK':
self.jwt = request.headers['jwt']
return True
raise Exception("Login failed")
def get_activity_stream(self, before=None):
"""
Get user's activity stream from
``https://www.duolingo.com/stream/<user_id>?before=<date> if before
date is given or else
``https://www.duolingo.com/activity/<user_id>``
:param before: Datetime in format '2015-07-06 05:42:24'
:type before: str
:rtype: dict
"""
if before:
url = "https://www.duolingo.com/stream/{}?before={}"
url = url.format(self.user_data.id, before)
else:
url = "https://www.duolingo.com/activity/{}"
url = url.format(self.user_data.id)
request = self._make_req(url)
try:
return request.json()
except:
raise Exception('Could not get activity stream')
def get_leaderboard(self, unit=None, before=None):
"""
Get user's rank in the week in descending order, stream from
``https://www.duolingo.com/friendships/leaderboard_activity?unit=week&_=time
:param before: Datetime in format '2015-07-06 05:42:24'
:param unit: maybe week or month
:type before: str
:type unit: str
:rtype: List
"""
if unit:
url = 'https://www.duolingo.com/friendships/leaderboard_activity?unit={}&_={}'
else:
raise Exception('Needs unit as argument (week or month)')
if before:
url = url.format(unit, before)
else:
raise Exception('Needs str in Datetime format "%Y.%m.%d %H:%M:%S"')
self.leader_data = self._make_req(url).json()
data = []
for result in iter(self.get_friends()):
for value in iter(self.leader_data['ranking']):
if result['id'] == int(value):
temp = {'points': int(self.leader_data['ranking'][value]),
'unit': unit,
'id': result['id'],
'username': result['username']}
data.append(temp)
return sorted(data, key=lambda user: user['points'], reverse=True)
def buy_item(self, item_name, abbr):
url = 'https://www.duolingo.com/2017-06-30/users/{}/purchase-store-item'
url = url.format(self.user_data.id)
data = {'name': item_name, 'learningLanguage': abbr}
request = self._make_req(url, data)
"""
status code '200' indicates that the item was purchased
returns a text like: {"streak_freeze":"2017-01-10 02:39:59.594327"}
"""
if request.status_code == 400 and request.json()['error'] == 'ALREADY_HAVE_STORE_ITEM':
raise AlreadyHaveStoreItemException('Already equipped with ' + item_name + '.')
if not request.ok:
# any other error:
raise Exception('Not possible to buy item.')
def buy_streak_freeze(self):
"""
figure out the users current learning language
use this one as parameter for the shop
"""
lang = self.get_abbreviation_of(self.get_user_info()['learning_language_string'])
if lang is None:
raise Exception('No learning language found')
try:
self.buy_item('streak_freeze', lang)
return True
except AlreadyHaveStoreItemException:
return False
def _switch_language(self, lang):
"""
Change the learned language with
``https://www.duolingo.com/switch_language``.
:param lang: Wanted language abbreviation (example: ``'fr'``)
:type lang: str
"""
data = {"learning_language": lang}
url = "https://www.duolingo.com/switch_language"
request = self._make_req(url, data)
try:
parse = request.json()['tracking_properties']
if parse['learning_language'] == lang:
self.user_data = Struct(**self._get_data())
except:
raise Exception('Failed to switch language')
def _get_data(self):
"""
Get user's data from ``https://www.duolingo.com/users/<username>``.
"""
get = self._make_req(self.user_url).json()
return get
@staticmethod
def _make_dict(keys, array):
data = {}
for key in keys:
if type(array) == dict:
data[key] = array[key]
else:
data[key] = getattr(array, key, None)
return data
@staticmethod
def _compute_dependency_order(skills):
"""
Add a field to each skill indicating the order it was learned
based on the skill's dependencies. Multiple skills will have the same
position if they have the same dependencies.
"""
# Key skills by first dependency. Dependency sets can be uniquely
# identified by one dependency in the set.
dependency_to_skill = MultiDict([(skill['dependencies_name'][0]
if skill['dependencies_name']
else '',
skill)
for skill in skills])
# Start with the first skill and trace the dependency graph through
# skill, setting the order it was learned in.
index = 0
previous_skill = ''
while True:
for skill in dependency_to_skill.getlist(previous_skill):
skill['dependency_order'] = index
index += 1
# Figure out the canonical dependency for the next set of skills.
skill_names = set([skill['name']
for skill in
dependency_to_skill.getlist(previous_skill)])
canonical_dependency = skill_names.intersection(
set(dependency_to_skill.keys()))
if canonical_dependency:
previous_skill = canonical_dependency.pop()
else:
# Nothing depends on these skills, so we're done.
break
return skills
def get_settings(self):
"""Get user settings."""
keys = ['notify_comment', 'deactivated', 'is_follower_by',
'is_following']
return self._make_dict(keys, self.user_data)
def get_languages(self, abbreviations=False):
"""
Get praticed languages.
:param abbreviations: Get language as abbreviation or not
:type abbreviations: bool
:return: List of languages
:rtype: list of str
"""
data = []
for lang in self.user_data.languages:
if lang['learning']:
if abbreviations:
data.append(lang['language'])
else:
data.append(lang['language_string'])
return data
def get_language_from_abbr(self, abbr):
"""Get language full name from abbreviation."""
for language in self.user_data.languages:
if language['language'] == abbr:
return language['language_string']
return None
def get_abbreviation_of(self, name):
"""Get abbreviation of a language."""
for language in self.user_data.languages:
if language['language_string'] == name:
return language['language']
return None
def get_language_details(self, language):
"""Get user's status about a language."""
for lang in self.user_data.languages:
if language == lang['language_string']:
return lang
return {}
def get_user_info(self):
"""Get user's informations."""
fields = ['username', 'bio', 'id', 'num_following', 'cohort',
'language_data', 'num_followers', 'learning_language_string',
'created', 'contribution_points', 'gplus_id', 'twitter_id',
'admin', 'invites_left', 'location', 'fullname', 'avatar',
'ui_language']
return self._make_dict(fields, self.user_data)
def get_certificates(self):
"""Get user's certificates."""
for certificate in self.user_data.certificates:
certificate['datetime'] = certificate['datetime'].strip()
return self.user_data.certificates
def get_streak_info(self):
"""Get user's streak informations."""
fields = ['daily_goal', 'site_streak', 'streak_extended_today']
return self._make_dict(fields, self.user_data)
def _is_current_language(self, abbr):
"""Get if user is learning a language."""
return abbr in self.user_data.language_data.keys()
def get_calendar(self, language_abbr=None):
"""Get user's last actions."""
if language_abbr:
if not self._is_current_language(language_abbr):
self._switch_language(language_abbr)
return self.user_data.language_data[language_abbr]['calendar']
else:
return self.user_data.calendar
def get_language_progress(self, lang):
"""Get informations about user's progression in a language."""
if not self._is_current_language(lang):
self._switch_language(lang)
fields = ['streak', 'language_string', 'level_progress',
'num_skills_learned', 'level_percent', 'level_points',
'points_rank', 'next_level', 'level_left', 'language',
'points', 'fluency_score', 'level']
return self._make_dict(fields, self.user_data.language_data[lang])
def get_friends(self):
"""Get user's friends."""
for k, v in iter(self.user_data.language_data.items()):
data = []
for friend in v['points_ranking_data']:
temp = {'username': friend['username'],
'id': friend['id'],
'points': friend['points_data']['total'],
'languages': [i['language_string'] for i in
friend['points_data']['languages']]}
data.append(temp)
return data
def get_known_words(self, lang):
"""Get a list of all words learned by user in a language."""
words = []
for topic in self.user_data.language_data[lang]['skills']:
if topic['learned']:
words += topic['words']
return set(words)
def get_learned_skills(self, lang):
"""
Return the learned skill objects sorted by the order they were learned
in.
"""
skills = [skill for skill in
self.user_data.language_data[lang]['skills']]
self._compute_dependency_order(skills)
return [skill for skill in
sorted(skills, key=lambda skill: skill['dependency_order'])
if skill['learned']]
def get_known_topics(self, lang):
"""Return the topics learned by a user in a language."""
return [topic['title']
for topic in self.user_data.language_data[lang]['skills']
if topic['learned']]
def get_unknown_topics(self, lang):
"""Return the topics remaining to learn by a user in a language."""
return [topic['title']
for topic in self.user_data.language_data[lang]['skills']
if not topic['learned']]
def get_golden_topics(self, lang):
"""Return the topics mastered ("golden") by a user in a language."""
return [topic['title']
for topic in self.user_data.language_data[lang]['skills']
if topic['learned'] and topic['strength'] == 1.0]
def get_reviewable_topics(self, lang):
"""Return the topics learned but not golden by a user in a language."""
return [topic['title']
for topic in self.user_data.language_data[lang]['skills']
if topic['learned'] and topic['strength'] < 1.0]
def get_translations(self, words, source=None, target=None):
"""
Get words' translations from
``https://d2.duolingo.com/api/1/dictionary/hints/<source>/<target>?tokens=``<words>``
:param words: A single word or a list
:type: str or list of str
:param source: Source language as abbreviation
:type source: str
:param target: Destination language as abbreviation
:type target: str
:return: Dict with words as keys and translations as values
"""
if not source:
source = self.user_data.ui_language
if not target:
target = list(self.user_data.language_data.keys())[0]
word_parameter = json.dumps(words, separators=(',', ':'))
url = "https://d2.duolingo.com/api/1/dictionary/hints/{}/{}?tokens={}" \
.format(target, source, word_parameter)
request = self.session.get(url)
try:
return request.json()
except:
raise Exception('Could not get translations')
def get_vocabulary(self, language_abbr=None):
"""Get overview of user's vocabulary in a language."""
if not self.password:
raise Exception("You must provide a password for this function")
if language_abbr and not self._is_current_language(language_abbr):
self._switch_language(language_abbr)
overview_url = "https://www.duolingo.com/vocabulary/overview"
overview_request = self._make_req(overview_url)
overview = overview_request.json()
return overview
_cloudfront_server_url = None
_homepage_text = None
@property
def _homepage(self):
if self._homepage_text:
return self._homepage_text
homepage_url = "https://www.duolingo.com"
request = self._make_req(homepage_url)
self._homepage_text = request.text
return self._homepage
@property
def _cloudfront_server(self):
if self._cloudfront_server_url:
return self._cloudfront_server_url
server_list = re.search('//.+\.cloudfront\.net', self._homepage)
self._cloudfront_server_url = "https:{}".format(server_list.group(0))
return self._cloudfront_server_url
_tts_voices = None
def _process_tts_voices(self):
voices_js = re.search('duo\.tts_multi_voices = {.+};',
self._homepage).group(0)
voices = voices_js[voices_js.find("{"):voices_js.find("}") + 1]
self._tts_voices = json.loads(voices)
def _get_voice(self, language_abbr, rand=False, voice=None):
if not self._tts_voices:
self._process_tts_voices()
if voice and voice != 'default':
return '{}/{}'.format(language_abbr, voice)
if rand:
return random.choice(self._tts_voices[language_abbr])
else:
return self._tts_voices[language_abbr][0]
def get_language_voices(self, language_abbr=None):
if not language_abbr:
language_abbr = list(self.user_data.language_data.keys())[0]
voices = []
if not self._tts_voices:
self._process_tts_voices()
for voice in self._tts_voices[language_abbr]:
if voice == language_abbr:
voices.append('default')
else:
voices.append(voice.replace('{}/'.format(language_abbr), ''))
return voices
def get_audio_url(self, word, language_abbr=None, random=True, voice=None):
if not language_abbr:
language_abbr = list(self.user_data.language_data.keys())[0]
tts_voice = self._get_voice(language_abbr, rand=random, voice=voice)
return "{}/tts/{}/token/{}".format(self._cloudfront_server, tts_voice,
word)
def get_related_words(self, word, language_abbr=None):
if not self.password:
raise Exception("You must provide a password for this function")
if language_abbr and not self._is_current_language(language_abbr):
self._switch_language(language_abbr)
overview_url = "https://www.duolingo.com/vocabulary/overview"
overview_request = self._make_req(overview_url)
overview = overview_request.json()
for word_data in overview['vocab_overview']:
if word_data['normalized_string'] == word:
related_lexemes = word_data['related_lexemes']
return [w for w in overview['vocab_overview']
if w['lexeme_id'] in related_lexemes]
attrs = [
'settings', 'languages', 'user_info', 'certificates', 'streak_info',
'calendar', 'language_progress', 'friends', 'known_words',
'learned_skills', 'known_topics', 'activity_stream', 'vocabulary'
]
for attr in attrs:
getter = getattr(Duolingo, "get_" + attr)
prop = property(getter)
setattr(Duolingo, attr, prop)
if __name__ == '__main__':
from pprint import pprint
duolingo = Duolingo('ferguslongley')
knowntopic = duolingo.get_known_topics('it')
pprint(knowntopic)