{ ^_^ } sinustrom Solving life, one problem at a time!

OTP car sweepstakes checker script

2019-09-16
Author: Zoltan Puskas
Categories: linux automation

Since most Hungarian banks do not pay much interest on certificate of deposit (CD) type of “investments” for some time now, but me still wanting to have some amount of easily accessible cash reserve, one thing I did was to get an OTP car sweepstakes deposit for fun, and potentially profit. However due to the nature of this CD one has to check each month whether it has won. Recently my automated checker script broke due to changes to OTP’s site, so it was time to update my script too.

Wait…, what is a “car sweepstake”?!

OTP car sweepstakes is a remnant form of “investment” (let’s face it, it’s not an investment, more of a game really) from the socialist era of the country, when if you wanted to buy a car you had to wait in the queue for 5-6 years, unless you won one through this type of deposit.

The gist of it is this: people deposit money into the bank, but instead of earning interest on it individually, the interest from all deposits is pooled together. Then the bank each month uses this pool of money to buy a few compact cars, and holds a draw between deposit holders, who may win one of the cars. If the car is not picked up within 30 days, then the value of the car is to be payed to the deposit holder. The values of such deposits can be 10k, 20k, and 50k HUF, where higher values have a higher chance of winning. One person can hold an arbitrary number of deposits, winnings do not lapse. You can at any time reclaim your deposit at the bank in the form of cash or into your checking account.

Let’s not get into the financial side of this. Though the chance of winning is still higher than the lottery the prize is also smaller, and while you can get your “ticket money” back, it’s obviously not a good investment. I made a single deposit of 50k HUF (equivalent to ~€151 or ~$167), because it’s an inconsequential portion of my cash reserves, and it’s barely worse than keeping a few banknotes at home. If I win I get a fun story out of it with some money, and if I don’t then eventually I’ll just cash it in and swallow the loss on inflation and the measly deposit interests I would have gotten on on it otherwise.

Checking for winnings

Naturally, not wanting to remember doing this every month and not wanting to do it manually, I automated it.

Old checker script

Before the electronic version was introduced not so long ago, you would get a small paper booklet with a number on it. When I got mine in 2014, the only ways to check if I won were to walk into a branch and to look at the bulletin board, or go to OTP’s website and enter the number manually.

Old OTP sweepstakes HTML form
Old OTP portal's checker form

Looking at the old website’s code it turned out to be a straightforward HTML page, which had an input form:

...
<form id="com_shiwaforce_otp_modules_gepkocsinyeremeny_command_box" action="?#gepkocsi_nyeremeny_box" method="post">
	<ul class="clearfix">
		<li><input id="sorszam" name="sorszam" class="search" type="text" value="" maxlength="9"/></li>
		<li><span class="search"><input type="submit" onclick="_gaq.push(['_trackEvent', 'Gepkocsinyeremeny', 'Button', 'Kereses']);" value="Keresés"/></span></li>
	</ul>
</form>
...

The important parts were the action and the input id that I had to send to the bank’s servers and then parse the resulting page. By entering a non-winning number and a recent winning number displayed on the page I also got the messages displayed on the page for each result respectively.

At this point the solution was straightforward, so I wrote a simple script, that would read ~/.gkny file containing for one or more booklet numbers, would make the HTTP POST request for me and look for one of the messages, and finally printing to the console if I won or not. I’ve also included a few reasonable error messages, in case something went weird and needed my attention.

#!/bin/sh
# Author: Zoltan Puskas <sinustrom.info>
# License: 3-clause BSD
# Date: 2014-2019

# URL to check for the winnings
GKURL="https://www.otpbank.hu/portal/hu/Megtakaritas/ForintBetetek/Gepkocsinyeremeny?#gepkocsi_nyeremeny_box"
# File name containing all the booklet numbers, one per line, placed in ~/
GKNY_FILE=".gkny"

user_file="${HOME}/${GKNY_FILE}"
if [[ -f "${user_file}" ]]; then
  while read -r line || [[ -n "$line" ]]; do
    if [[ -z $line ]]; then
        continue
    fi
    # Send the form request to OTP's servers
    web_data=$(curl -s -d "sorszam=${line}" "${GKURL}")
    if [ $? -eq 0 ]; then
      # Got a result page, search for the messages
      lose=$(grep "Sajnos, az Ön által megadott ${line} sorszámú gépkocsinyeremény-betét nem nyert." <<< "${web_data}")
      win=$(grep "Az Ön ${line} számú gépkocsinyeremény betétjét kisorsolták!" <<< "${web_data}")

      if [[ -z $win && -n $lose ]]; then
        # Did not win, maybe next time
        echo "A #${line} gépkocsinyeremény betétkönyv nem nyert."
      elif [[ -z $lose && -n $win ]]; then
        # I won!
        echo "A #${line} gépkocsinyeremény betétkönyv nyert!"
      else
        # Failed page parsing
        echo "Sikertelen értelmezés! :("
      fi
    else
      # Page not available
      echo "A weblap nem elérhető!"
    fi
  done < $user_file
else
    # Config not found
    echo "${user_file} nem elérhető!"
fi

Once tested, I’ve placed the script into /usr/local/bin, made it executable and added a cron entry to run it once a month and email me the result:

0 7 17 * * /usr/local/bin/gkny-check.sh | mail -s "[gkny] Monthly check" -a "Context-Type: text-plain; charset=\"UTF-8\"" $USER@localhost

New checker script

As this month’s check has failed, I went investigating, and started by opening up the portal in my browser. First thing to notice is that the format of the form has changed and now it takes two fields: series number and serial number of the booklet instead of the concatenation of the two.

New OTP sweepstakes HTML form
New OTP portal's checker form

I proceeded to fetching the source of the page,

$ curl 'https://www.otpbank.hu/portal/hu/Megtakaritas/ForintBetetek/Gepkocsinyeremeny' > /tmp/otp.html

but unlike last time, when they simply rephrased their messages, now there was a major rewrite on the portal and a simple quick fix would not be possible.

Searching through the source code I found no HTML forms. At this point I pulled up Firefox’s debugger and used the element inspector to examine the rendered form on the fully loaded page. This is what I got:

HTML source of the rendered new form
HTML source of the rendered new form

Apparently I’m supposed to be looking for something along the lines of “car-sweepstakes-widget”, so I went back to the fetched page source, but I only found a placeholder div container:

...
<a aria-hidden="true" class="bookmark" id="KERESO" data-text=""></a>
<car-sweepstakes-widget branch-atm-page-url="/portal/hu/Kapcsolat/Fiokkereso" v-cloak=""></car-sweepstakes-widget>
<div class="content-placeholder-content" aria-hidden="true">
  <h3></h3>
  <div class="car-sweepstakes-widget--placeholder content-placeholder">
    <div class="content-placeholder-animated-background content-placeholder-animated-background--green">
      <div class="content-mask content-mask-1"></div>
...

Now we know why there is no input form on the page: it most likely relies on JavaScript for rendering and handling it. Examining the source a bit more I find the following reference:

<script defer="" src="/static/portal/applications/car-sweepstakes-widget.3319ca28bfa9d5c00b88.bundle.js"></script>

Fetching the JavaScript source will give us a minified one liner JS file. Putting it into an online JS beautifier made the code at least somewhat readable and skimming through the result pretty much confirmed my suspicion.

I’ve continued investigating to see if there is a way for me to submit a request to the bank’s servers. After all, the script does it somehow too. Instead of reading through 1500+ lines of minified JS, I went back to Firefox’s debugger, but this time to the network tab and executed a check with one of the winning numbers on the page:

New request API call
New request API call

This is actually pretty promising as it sends a GET request to an API URL and gets a JSON response back. Let’s see it on the console, this time trying both with a winning and non-winning number:

$ curl https://www.otpbank.hu/apps/composite/api/carsweepstakes/check/601261452
{"number":"601261452","sweepstakes":[{"lotDate":"2019-09-16","carType":"Toyota Yaris 1,0 Live 5 ajtós"}]}%
$ curl https://www.otpbank.hu/apps/composite/api/carsweepstakes/check/600123456
{"number":"600123456","sweepstakes":[]}%

Grand, we have ourselves an API! It seems the only thing we need to do is parse the JSON result and see if “sweepstakes” key has values or not. As a quick sanity check I’ve searched a few keywords in the JS file, namely: “sweepstakes”, “lotDate”, “carType”, and “composite/api” and read parts of the code to see if there are any edge cases that I might need to be looking out for. As it turns out, not really.

With all this knowledge it was time to whip up a new checker script, but this time I chose to go with Python.

#!/usr/bin/env python3
# Author: Zoltan Puskas <sinustrom.info>
# License: GPLv3
# Date: 2019-09-16

import json
import os
import sys
import urllib.request

API_URL = "https://www.otpbank.hu/apps/composite/api/carsweepstakes/check/{num}"
GKNY_FILE = ".gkny"


def perform_checks():
    # Read sweepstake numbers from user's configuration file
    try:
        with open("{}/{}".format(os.getenv("HOME"), GKNY_FILE), 'r') as config:
            numbers = [line.strip() for line in config.readlines()]
    except (FileNotFoundError, PermissionError) as e:
        print(e, file=sys.stderr)
        return e.errno

    # Process each number
    for number in numbers:
        # Fetch data from OTP servers
        try:
            response = urllib.request.urlopen(API_URL.format(num=number)).read()
        except Exception as e:
            print("Query to OTP server has failed with: {}".format(e.reason),
                  file=sys.stderr)
            return e.errno

        # Parse data
        try:
            data = json.loads(response)
        except json.JSONDecodeError as e:
            print("Parsing JSON response failed with: {}".format(e.msg),
                  file=sys.stderr)
            return 76  # Remote error in protocol

        # Only print to the output if we won!
        try:
            if len(data['sweepstakes']):
                print(f"Sweepstake {number} has won!")
        except KeyError:
            print("JSON format has changed, sweepstake check failed!",
                  file=sys.stderr)
            return 76  # Remote error in protocol

    return 0


if __name__ == "__main__":
    sys.exit(perform_checks())

I’ve replaced my old script in /usr/local/bin with this one and updated my crontab to:

0 7 17 * * /usr/local/bin/gkny-check.py 2>&1 | mail -E -s "[gkny] Monthly check" -a "Context-Type: text-plain; charset=\"UTF-8\"" $USER@localhost

Additionally the new script will have output only if there is an error (Python execution failure or any uncaught exceptions will also result in some output) or I have won. This way in combination with -E switch on mail I will only be notified if there is something actionable otherwise the script will just work silently in the background, reducing automated mail noise.

Alternative methods

Reading through the new page I’ve noticed that now they offer other methods for checking:

  • via a mobile app
  • on the internet banking profile after logging in

None of these options are good. The former requires installing a closed source app on my phone, while the latter only works for newly acquired E-Sweepstakes, and there is no way of registering the paper based ones. What’s worse both of these methods still require manual checking.

Finally I’ve also discarded using a scraper while automating this, i.e. by using Selenium, to execute the JavaScript page, as it would have taken more time, and would have resulted in a much more complicated and resource intensive solution than a simple script exercising an API call.


Content