Skip to content

Instantly share code, notes, and snippets.

@JohnMertz
Created June 18, 2019 21:59
Show Gist options
  • Save JohnMertz/c869402cb8f54d14a3c1f8a0a54268af to your computer and use it in GitHub Desktop.
Save JohnMertz/c869402cb8f54d14a3c1f8a0a54268af to your computer and use it in GitHub Desktop.
Duolingo API Script
#!/usr/bin/perl
use strict;
use warnings;
use WWW::Mechanize;
use JSON::Any;
use HTTP::CookieJar::LWP;
my $user = 'username';
my $pass = 'password';
sub new {
my %params = @_;
if (!defined $params{user} || !defined $params{pass}) {
die "'new' requires your 'user' and 'pass' key values\n";
}
my $cookie_jar = HTTP::CookieJar::LWP->new();
if (!defined $params{mech}) {
$params{mech} = WWW::Mechanize->new(autocheck => 1, cookie_jar => $cookie_jar, agent => 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDRJS; rv:11.0) like Gecko');
}
my $self = { username => $params{user}, password => $params{password}, mechanize => $params{mech}, base_url => 'https://www.duolingo.com' };
bless $self;
$self->authenticate();
return $self;
}
sub authenticate {
my $self = shift;
my $response = $self->{mechanize}->post($self->{base_url} . '/login', [ 'login' => $user, 'password' => $pass ]);
my $json = $response->decoded_content();
my $content =JSON::XS::decode_json($json);
unless ( $content->{response} eq 'OK' ) {
die "Authentication Failed\n";
}
$self->{user_id} = $content->{user_id};
my $headers = $response->headers();
$self->{jwt} = $headers->{jwt};
}
sub response {
my $self = shift;
my %params = @_;
if (!defined $self->{jwt}) {
die "Session has not yet been authenticated. Must initialize with 'new'.\n";
}
my $response = $self->{mechanize}->get($self->{base_url} . $params{resource} );
my $json = $response->decoded_content();
my $content = JSON::XS::decode_json($json);
return $content;
}
my $duo = new( user => $user, pass => $pass );
# The regular API requires the tedious work of finding all activity
# timestamped for today, then adding up the xp to provide a total.
# I only want the streak length, goal, and today's xp, so I'm going
# to be lazy and use an older API that does the tally for me.
$duo->{base_url} = 'https://www.duolingo.com/2017-06-30';
# The fetch line is the slightly different because the current API
# uses the 'username', while the old one uses the 'user_id'. Luckily,
# I stored the 'user_id' during authentication!
# my $data = $duo->response(resource => "/users/" . $self->{username});
my $data = $duo->response(resource => "/users/" . $duo->{user_id});
# Note: The data collected is structured different for each API
# version, so the following won't work for the current one. I also only
# print the values I care about. Dump the data structure to find which
# values you need:
# use Data::Dump 'dump';
# print Data::Dump::dump($data);
# The current day's xp isn't entirely simple to fetch in the old API
# either. The only place that it is pre-calculated is in an array of
# global achievements:
my $xp;
my @achievements = @{$data->{_achievements}};
foreach (@achievements) {
if ($_->{name} eq 'xp') {
$xp = $_->{count};
last;
}
}
print JSON::XS::encode_json { goal_met => $data->{xpGoalMetToday}, streak => $data->{streakData}->{length}, xp => $xp . '/' . $data->{streakData}->{xpGoal} } ;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment