KartikTalwar / Duolingo

Unofficial Duolingo API Written in Python
MIT License
825 stars 128 forks source link

buy_streak_freeze() always buys a new streak #91

Open andreasscherbaum opened 4 years ago

andreasscherbaum commented 4 years ago

Started a couple days ago: every time I run buy_streak_freeze(), I don't get a AlreadyHaveStoreItemException exception anymore, but it started "charging" me Lingots every time - even though I already have a streak equipped.

Anyone knows what's wrong there, or how to fix/prevent that?

igorskh commented 4 years ago

Looks like it's not a bug, but an expected behaviour, there is a different response now:

{
  "purchaseId": "d67b790c25fd2203c4ee36617f6141a1",
  "purchaseDate": 1597765436,
  "purchasePrice": 10,
  "id": "streak_freeze",
  "itemName": "streak_freeze",
  "quantity": 3
}

The quantity is just increasing, I assume it's like buying "in advance".

I think getting /2017-06-30/users/{userID}?fields=shopItems and checking for streak_freeze quantity before initiating a purchase is a solution.

andreasscherbaum commented 4 years ago

So I can buy streaks in advance for as long as I have Lingots? ;-)

Hmmm

andreasscherbaum commented 4 years ago

Well, it's still not what I expect buy_streak_freeze() to do ...

igorskh commented 4 years ago

So I can buy streaks in advance for as long as I have Lingots? ;-)

I suppose so. I mean, why not? It might be useful in some cases, one can plan few days away in advance. Although, I am not sure if it works this way, it is just my assumption.

Well, it's still not what I expect buy_streak_freeze() to do ...

In my opinion it does what it says. It buys a streak freeze :) You can think about some additional method like restore_streak_freeze(), but it's some additional feature, not API related.

The question is more to Duolingo API developers rather to this library.

igorskh commented 4 years ago

Ok, i've tested it more. It does not go more than 3 purchased items, although lingots are charged..

andreasscherbaum commented 4 years ago

@igorskh So basically "quantity" is only known after a purchase?

igorskh commented 4 years ago

@igorskh So basically "quantity" is only known after a purchase?

No, it's not, you can always check current quantity here: https://www.duolingo.com/2017-06-30/users/{userID}?fields=shopItems

It returns all purchased items including streak_freeze:

[
    {
      "quantity": 3,
      "purchaseDate": 1597768970,
      "id": "streak_freeze",
      "purchaseId": "2abbdba95ae154068f9202a37529e141",
      "itemName": "streak_freeze",
      "purchasePrice": 0
    }
]
andreasscherbaum commented 4 years ago

Looking at the documentation for this library, I don't see a method to retrieve that information. Am I correct?

https://github.com/KartikTalwar/Duolingo

efodor commented 4 years ago

I can confirm the behavior of the API has changed. It buys the streak regardless of whether or not you already have one purchased and deducts additional lingots. It does not appear to give you extra "freezes", just burns through lingots faster. I suggest adding a get_streak_freeze() method that returns True or False if the streak is active (assuming that's possible) or something similar.

alexsanjoseph commented 4 years ago

I've a workaround until the API/library is fixed -

def item_already_equipped(lingo, item):
    if item == 'streak_freeze':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['num_item_streak_freeze'] > 0
    if item == 'rupee_wager':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['has_item_rupee_wager']

def process_single_user(username, password):
    import duolingo
    try:
        lingo = duolingo.Duolingo(username, password)
    except ValueError:
        raise Exception("Username Invalid")

    stuff_to_purchase = ['streak_freeze', 'rupee_wager']

    for item in stuff_to_purchase:
        if(item_already_equipped(lingo, item)):
            print("Item "+ item + " already equipped! Skipping...")
            continue
        try:
            print("Trying to Buy " + item + " for " + username)
            lingo.buy_item(item, 'en')
            print("Bought " + item + " for " + username)
        except duolingo.AlreadyHaveStoreItemException: # no longer triggered AFAIK
            print("Item Already Equipped")
        except Exception:
            raise ValueError("Unable to buy " + item)
igorskh commented 4 years ago

Did anyone test that quantity of streak_freeze decreases after being used?

I am not entirely sure, but I think I could increase it from 1 to 3.

alexsanjoseph commented 4 years ago

Planning to keep my streak on line today for testing. Will report back if it reduces to less than 3

igorskh commented 4 years ago

I can confirm that quantity increases

> GET /2017-06-30/users/665585457?fields=shopItems
> Host: www.duolingo.com
> user-agent: insomnia/2020.3.3

< HTTP/2 200 
< date: Fri, 21 Aug 2020 19:20:39 GMT
< content-type: application/json
{
  "shopItems": [
    {
      "quantity": 1,
      "purchaseDate": 1598037464,
      "id": "streak_freeze",
      "itemName": "streak_freeze",
      "purchasePrice": 0
    }
  ]
}

> POST /2017-06-30/users/665585457/shop-items HTTP/2
> Host: www.duolingo.com
> user-agent: insomnia/2020.3.3
| {
|   "itemName": "streak_freeze",
|   "learningLanguage": "es"
| }

< HTTP/2 200 
< date: Fri, 21 Aug 2020 19:44:30 GMT
< content-type: application/json
{
  "purchaseDate": 1598039070,
  "purchasePrice": 10,
  "id": "streak_freeze",
  "itemName": "streak_freeze",
  "quantity": 2
}

> GET /2017-06-30/users/665585457?fields=shopItems
> Host: www.duolingo.com
> user-agent: insomnia/2020.3.3

< HTTP/2 200 
< date: Fri, 21 Aug 2020 19:47:19 GMT
< content-type: application/json
{
  "shopItems": [
    {
      "quantity": 2,
      "purchaseDate": 1598039070,
      "id": "streak_freeze",
      "itemName": "streak_freeze",
      "purchasePrice": 0
    }
  ]
}
igorskh commented 4 years ago

From what I see, this is also not returned anymore:

https://github.com/KartikTalwar/Duolingo/blob/6d3e770f0987d3de370519aae030e4bf6fb11984/duolingo.py#L182-L183

It just doesn't return anything now, and script crashes

simplejson.errors.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
igorskh commented 4 years ago

Quantity is decreasing as well, so it makes sense to allow buying more than 1 streak, even current limit to 3 is maybe just a bug in the API, or they are testing, at the end it is not an official feature.

efodor commented 4 years ago

It appears that if you purchase multiple freezes, the freeze is extended for multiple days. My current quantity is now at 2 (was 3 yesterday), but the UI still showed the freeze in place. It looks like they might be adding a multi day streak feature (or at least it appears to be a hidden feature).

TiloGit commented 4 years ago

@alexsanjoseph how can we use your work around. Could you please explain where we need to paste it and what we need to call to make it happen.

alexsanjoseph commented 4 years ago

@TiloGit - I run this as a Lambda function in AWS (https://github.com/alexsanjoseph/duolingo-save-streak).

The issue is that Duolingo keeps buying streak freeze even when it has reached the limit. Before this was enforced as a limit from their API and hence we could do EAFP and catch the error without an issue. However, the lingo class already has info on whether to buy it or not, and so you purchase it only when you check and identify that you don't have the particular item in your bag.

def item_already_equipped(lingo, item):
    if item == 'streak_freeze':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['num_item_streak_freeze'] > 0
    if item == 'rupee_wager':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['has_item_rupee_wager']

This function does the checking.

TiloGit commented 4 years ago

here my hack for other ppl info. (not pretty but seem to do the job) https://gist.github.com/TiloGit/51367aca5a4df726a7061288d8430828