Skip to content

puntonim/reborn-automator

Repository files navigation

Reborn Automator

Automate the booking of classes at Reborn gym.


This project is deployed to AWS Lambda and it is triggered by cron events scheduled with EventsBridge Scheduler.

At the right time, as per the cron schedule, the Lambdas try to book the next calisthenics or powerlifting class, and send me a Telegram message with the result.

⚡ Usage

There is no HTTP interface (apart from the introspection HTTP endpoint), but just 2 cron-scheduled Lambdas. So the automatic triggers are the cron schedules.

📐 Architecture

The Lambda functions are triggered by a cron schedule in EventBridge Scheduler:

  • calisthenics:
    • cron(2 20 ? * SAT,MON *) # Every Saturday and Monday at 20:02 Italian timezone.
  • powerlifting:
    • cron(2 19 ? * SUN,TUE *) # Every Sunday and Tuesday at 19:02 Italian timezone

These times work with the business rules explained in How it works.

To send Telegram messages, we use Botte (botte-monorepo) via its Lambda interface.

No database.

architecture-draw.io.svg

🔭 How it works

Classes are usually booked via a mobile app that performs regular HTTP(s) requests.

I inspected these requests by installing the app on the emulator Genymotion on macOS and intercepting the traffic using HTTP Toolkit.
Find the details of these requests in the next sections.

Some business rules are in place in order to regulate bookings:

  • calisthenics class:
    • booking for Monday 20:00 classes opens the previous Saturday at 20:00
    • booking for Wednesday 20:00 classes opens the previous Monday at 20:00
  • powerlifting class:
    • booking for Tuesday 19:00 classes opens the previous Sunday at 19:00
    • booking for Thursday 19:00 classes opens the previous Tuesday at 19:00
      So these times are the cron schedule.

1' request: login

Login using username and password and get the codice_sessione.

$ curl -X POST https://reborn.shaggyowl.com/funzioniapp/v407/loginApp \
 -d "[email protected]&pass=mypassword&id_sede=47" \
 -H "user-agent: Dart/3.1 (dart:io)"

{
    "status": 2,
    "messaggio": "Accesso effettuato con successo.",
    "parametri": {
        "sessione": {
            "isFacebook": 0,
            "idFacebook": "",
            "isGoogle": 0,
            "idGoogle": "",
            "idSede": "",
            "idCliente": "206999",
            "statoCliente": "",
            "nomeCliente": "Paolo Rossi",
            "cognomeCliente": "",
            "mail": "[email protected]",
            "pass": "",
            "codice_sessione": "1729800784xxxxxxxxxx",
            "path_img": "https://storage.shaggyowl.com/myapp/immagini/default/thumb-app-palestre.jpg",
            "path_img_big": "https://storage.shaggyowl.com/myapp/immagini/default/thumb-app-palestre.jpg",
            "sede": "{}",
            "cliente": {},
        },
        "sedi_collegate": [
            {
                "nome": "Reborn",
                "id_sede": "47",
                "codice": "jkdGu3stmetropreborn",
                "path_img": "image-picker-0e310cc0-cdc8-49cf-a514-83d17b522f77-1397-0000016136ebe886-1829569936.png",
                "path_img_list": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_sedi/152_152/image-picker-0e310cc0-cdc8-49cf-a514-83d17b522f77-1397-0000016136ebe886-1829569936.png",
                "path_img_big": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_sedi/image-picker-0e310cc0-cdc8-49cf-a514-83d17b522f77-1397-0000016136ebe886-1829569936.png",
                "path_img_inner": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_sedi/250_640/image-picker-0e310cc0-cdc8-49cf-a514-83d17b522f77-1397-0000016136ebe886-1829569936.png",
                "active": 1,
                "comune": "Torre Boldone",
                "telefono": "3336286549",
                "web": "http://www.youreborn.it/",
                "mail": "[email protected]",
                "facebook": "https://www.facebook.com/yourebornofficial/",
                "twitter": "",
                "google": "",
                "instagram": "https://www.instagram.com/yourebornofficial/?hl=it",
                "testo": '<h6><span style="font-weight: normal;"><span style="font-size: 12px;"><i>Palestra funzionale presente sul territorio bergamasco dal 2011.<br></i></span><span style="font-size: 12px;"><i>La nostra forza sono l’elasticità e la professionalità. Lavoriamo con il cliente per essere sicuri di raggiungere ogni obiettivo, piccolo o grande che sia.</i></span></span></h6>',
                "distance": "",
                "indirizzo_completo": "Torre Boldone, Largo delle Industrie 9, 24020 Bergamo, LOMBARDIA",
                "lat": "45.7096864",
                "lon": "9.7141489",
                "listaPagine": [
                    "home_utente_crossfit",
                    "palinsesto",
                    "wod",
                    "info_cliente",
                ],
                "id_stato_lavorazione": "3",
                "lista_utenti": [],
                "impostazioni_sede": {
                    "on_boarding": {
                        "on_boarding_attivo": "2",
                        "carta_di_credito_obbligatoria": "1",
                        "on_boarding_sezioni": [
                            {
                                "key": "dati_utente",
                                "campi_obbligatori": [
                                    "1",
                                    "2",
                                    "7",
                                    "8",
                                    "9",
                                    "12",
                                    "29",
                                ],
                                "tipologie": [],
                            },
                            {
                                "key": "documenti",
                                "campi_obbligatori": [],
                                "tipologie": ["attestati_medici"],
                            },
                            {
                                "key": "regolamenti",
                                "campi_obbligatori": ["regolamento"],
                                "tipologie": [],
                            },
                        ],
                    },
                    "sezioni_da_nascondere": [
                        "servizi",
                        "prenotazioni_campi",
                        "shop",
                        "multimedia",
                        "disponibilita",
                        "schede",
                        "Multimedia",
                    ],
                    "impostazioni_abbonamenti": "",
                    "struttura_multimedia": [],
                    "filtri_staff_disponibilita": None,
                    "compenso_staff_ore_no_preno": "2",
                    "compenso_corsi_non_prenotabili": "2",
                    "mostra_priorita": "1",
                    "impostazioni_personalizzazione_app": {
                        "primary_color_light": "#4d6fb2",
                        "primary_color_dark": "#4d6fb2",
                        "theme_mode": "both",
                        "bottom_bar": [
                            "home_utente_crossfit",
                            "palinsesto",
                            "wod",
                            "info_cliente",
                        ],
                        "placeholder_pers_list": None,
                        "bottom_bar_style": "Stile 1",
                        "bottom_bar_fab": "servizi",
                        "nascondere_numero_di_prenotati": "1",
                        "permetti_modifica_livello": "1",
                        "livelli": [
                            {"id": "0", "nome": "Nessuno"},
                            {"id": "1", "nome": "Base"},
                            {"id": "2", "nome": "Base-Intermedio"},
                            {"id": "3", "nome": "Intermedio"},
                            {"id": "4", "nome": "Intermedio-Avanzato"},
                            {"id": "5", "nome": "Avanzato"},
                        ],
                        "etichette": [
                            {"id": "palinsesto", "nome": "Palinsesto"},
                            {"id": "wod", "nome": "WOD"},
                            {"id": "disponibilita", "nome": "Personal"},
                            {"id": "schede", "nome": "Schede"},
                            {"id": "abbonamenti", "nome": "Abbonamenti"},
                            {"id": "sessioni", "nome": "Sessioni"},
                            {"id": "wod_log", "nome": "WOD Log"},
                            {"id": "corsi", "nome": "Corsi"},
                            {"id": "prenotazioni", "nome": "Prenotazioni"},
                            {"id": "prenotazioni_campi", "nome": "Campetti"},
                            {"id": "info_cliente", "nome": "Le tue info"},
                            {"id": "sede", "nome": "Centro"},
                            {"id": "servizi", "nome": "Servizi"},
                        ],
                        "font": "Poppins",
                    },
                },
                "myapp": "2",
                "indirizzo_android": "https://play.google.com/store/apps/details?id=com.shaggyowl.reborn&hl=it&gl=US",
                "indirizzo_ios": "https://apps.apple.com/it/app/youreborn/id1398754364",
            }
        ],
    },
}

2' request: get palinsesto

Get the id_orario_palinsesto for the class you are interested. You might skip this if you already know it and if it does not change over time.

$ curl -X POST https://reborn.shaggyowl.com/funzioniapp/v407/palinsesti \
   -d 'id_sede=47&codice_sessione=172978952xxx&giorno=2024-10-25'
   -H "user-agent: Dart/3.1 (dart:io)"

{
    "status": 2,
    "messaggio": "Tutto bene",
    "parametri": {
        "lista_risultati": [
            {
                "id_palinsesti": "47",
                "nome_palinsesto": "Lezioni Collettive",
                "visibile": "2",
                "principale": "2",
                "tipo": "palinsesto",
                "idclienti": "",
                "id_cliente": "0",
                "is_all_visible": "2",
                "note": "",
                "idclienti_array": [],
                "tagsc": [],
                "tagsc_value": "",
                "tagsa": [],
                "tagsa_value": "",
                "giorni": [
                    {
                        "orari_giorno": [
                            {
                                "id_orario_palinsesto": "748396",
                                "is_online": "1",
                                "no_greenpass": "1",
                                "a_crediti": "1",
                                "crediti": "0",
                                "orario_inizio": "19:30",
                                "orario_fine": "20:30",
                                "via": "",
                                "lat": "",
                                "lon": "",
                                "nota": "",
                                "nome_corso": "Aerobic Capacity",
                                "prenotabile_corso": "2",
                                "iscrizioni": "2",
                                "ingressi_corso": "1",
                                "color_corso": "#339966",
                                "prezzo": "0.00",
                                "path_img_corso": "",
                                "path_img_list_corso": "",
                                "path_img_inner_corso": "",
                                "path_img_big_corso": "",
                                "path_img_small_corso": "",
                                "staff": {
                                    "principali": [
                                        {
                                            "id_staff": "1649",
                                            "nome": "Maria Luisa Bottini",
                                            "color": "#a000ff",
                                        }
                                    ],
                                    "secondari": [],
                                },
                                "nome_staff": "Maria Luisa Bottini",
                                "nome_stanza": "",
                                "nome_campo": "",
                                "blocco_coda": 0,
                                "multimedia": "1",
                                "prenotazioni": {
                                    "numero_posti_disponibili": "9",
                                    "numero_utenti_coda": "0",
                                    "numero_utenti_attesa": "0",
                                    "numero_posti_occupati": "6",
                                    "id_disponibilita": "0",
                                    "nota": "",
                                    "utente_prenotato": "0",
                                    "frase": "Prenotazioni chiuse",
                                    "prenota_coda": "2",
                                },
                            },
                            {
                                "id_orario_palinsesto": "758744",
                                "is_online": "1",
                                "no_greenpass": "1",
                                "a_crediti": "1",
                                "crediti": "0",
                                "orario_inizio": "20:00",
                                "orario_fine": "21:00",
                                "via": "",
                                "lat": "",
                                "lon": "",
                                "nota": "",
                                "nome_corso": "Calisthenics",
                                "prenotabile_corso": "2",
                                "iscrizioni": "2",
                                "ingressi_corso": "1",
                                "color_corso": "#ff0000",
                                "prezzo": "0.00",
                                "path_img_corso": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_corsi/4-393962289.png",
                                "path_img_list_corso": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_corsi/152_152/4-393962289.png",
                                "path_img_inner_corso": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_corsi/250_640/4-393962289.png",
                                "path_img_big_corso": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_corsi/4-393962289.png",
                                "path_img_small_corso": "https://storage.shaggyowl.com/myapp/immagini/img_rappr_corsi/0/small/4-393962289.png",
                                "staff": {
                                    "principali": [
                                        {
                                            "id_staff": "1654",
                                            "nome": "Matteo Artina",
                                            "color": "#0084ff",
                                        }
                                    ],
                                    "secondari": [],
                                },
                                "nome_staff": "Matteo Artina",
                                "nome_stanza": "",
                                "nome_campo": "",
                                "blocco_coda": 0,
                                "multimedia": "1",
                                "prenotazioni": {
                                    "numero_posti_disponibili": "0",
                                    "numero_utenti_coda": "0",
                                    "numero_utenti_attesa": "0",
                                    "numero_posti_occupati": "16",
                                    "id_disponibilita": "0",
                                    "nota": "",
                                    "utente_prenotato": "10992911",
                                    "frase": "Sei prenotato per questo orario (16 p.)",
                                    "prenota_coda": "2",
                                },
                            },
                        ],
                        "nome_giorno": "Venerdì 25/10/2024",
                        "giorno": "2024-10-25",
                    },
                    ...
                ],
            },
            {
                "id_palinsesti": "1931",
                "nome_palinsesto": "Open Gym",
                "visibile": "2",
                "principale": "1",
                "tipo": "palinsesto",
                "idclienti": "",
                "id_cliente": "0",
                "is_all_visible": "2",
                "note": "",
                "idclienti_array": [],
                "tagsc": [],
                "tagsc_value": "",
                "tagsa": [],
                "tagsa_value": "",
                "giorni": [
                    {
                        "orari_giorno": [],
                        "nome_giorno": "Giovedì 24/10/2024",
                        "giorno": "2024-10-24",
                    },
                    {
                        "orari_giorno": [],
                        "nome_giorno": "Venerdì 25/10/2024",
                        "giorno": "2024-10-25",
                    },
                    ...
                ],
            },
        ]
    },
}

3' request: book class

Finally book the class.

$ curl -X POST https://reborn.shaggyowl.com/funzioniapp/v407/prenotazione_new \
   -d 'id_sede=47&codice_sessione=1729789529xxx&id_orario_palinsesto=719536&data=2024-10-25'
   -H "user-agent: Dart/3.1 (dart:io)"

{"status": 1, "messaggio": "Prenotazioni non aperte.", "parametri": {}}

🛠️ Development setup

1 - System requirements

Python 3.13
The target Python 3.13 as it is the latest available environment at AWS Lambda.
Install it with pyenv:

$ pyenv install -l  # List all available versions.
$ pyenv install 3.13.7

Poetry
Pipenv is used to manage requirements (and virtual environments).
Read more about Poetry here.
Follow the install instructions.

Pre-commit
Pre-commit is used to format the code with black before each git commit:

$ pip install --user pre-commit
# On macOS you can also:
$ brew install pre-commit

2 - Virtual environment and requirements

Create a virtual environment and install all deps with one Make command:

$ make poetry-create-env
# Or to recreate:
$ make poetry-destroy-and-recreate-env
# Then you can open a shell and/or install:
$ poetry shell

Without using Makefile the full process is:

# Activate the Python version for the current project:
$ pyenv local 3.13  # It creates `.python-version`, to be git-ignored.
$ pyenv which python
/Users/nimiq/.pyenv/versions/3.13.7/bin/python

# Now create a venv with poetry:
$ poetry env use ~/.pyenv/versions/3.13.7/bin/python
# Now you can open a shell and/or install:
$ eval $(poetry env activate)
# And finally, install all requirements:
$ poetry install
# And later deactivate the virtual env with:
$ deactivate

To add new requirements:

$ poetry add requests

# Dev or test only.
$ poetry add -G test pytest
$ poetry add -G dev ipdb

# With extra reqs:
$ poetry add -G dev "aws-lambda-powertools[aws-sdk]"
$ poetry add "requests[security,socks]"

# From Git:
$ poetry add git+https://github.com/aladagemre/django-notification

# From a Git subdir:
$ poetry add git+https://github.com/puntonim/utils-monorepo#subdirectory=log-utils
# and with extra reqs:
$ poetry add "git+https://github.com/puntonim/utils-monorepo#subdirectory=log-utils[rich-adapter,loguru-adapter]"
# and at a specific version:
$ poetry add git+https://github.com/puntonim/utils-monorepo@00a49cb64524df19bf55ab5c7c1aaf4c09e92360#subdirectory=log-utils
# and at a specific version, with extra reqs:
$ poetry add "git+https://github.com/puntonim/utils-monorepo@00a49cb64524df19bf55ab5c7c1aaf4c09e92360#subdirectory=log-utils[rich-adapter,loguru-adapter]"

# From a local dir:
$ poetry add ../utils-monorepo/log-utils/
$ poetry add "log-utils @ file:///Users/myuser/workspace/utils-monorepo/log-utils/"
# and with extra reqs:
$ poetry add "../utils-monorepo/log-utils/[rich-adapter,loguru-adapter]"
# and I was able to choose a Git version only with pip (not poetry):
$ pip install "git+file:///Users/myuser/workspace/utils-monorepo@00a49cb64524df19bf55ab5c7c1aaf4c09e92360#subdirectory=log-utils" 

3 - Pre-commit

$ pre-commit install

🔨 Test

To run unit and end-to-end tests:

$ make test

🚀 Deployment

1. Install deployment requirements

The deployment is managed by Serverless. Serverless requires NodeJS.
Follow the install instructions for NVM (Node Version Manager).
Then:

$ nvm install --lts
$ node -v > .nvmrc

Follow the install instructions for Serverless, something like curl -o- -L https://slss.io/install | bash. We currently use version 4.23.0, if you have an older major version you can upgrade Serverless with: sls upgrade --major.

Then to install the Serverless plugins required:

#$ sls upgrade  # Only if you are sure it will not install a major version.
$ nvm install
$ nvm use

2. Deployments steps

2a. AWS Parameter Store

Add to AWS Parameter Store the Reborn app creds.
Keys:

  • /reborn-automator/prod/reborn-creds-username
  • /reborn-automator/prod/reborn-creds-password

2b. Actual deploy

Note: AWS CLI and credentials should be already installed and configured.\

Finally, deploy to PRODUCTION in AWS with:

$ sls deploy
# $ make deploy  # Alternative.

To deploy a single function (only if it was already deployed):

$ sls deploy function -f endpoint-health

Deploy to a DEV STAGE

Pick a stage name: if your name is Jane then the best format is: dev-jane.
Create the keys in AWS Parameter Store with the right stage name.

To deploy your own DEV STAGE in AWS version:

# Deploy:
$ sls deploy --stage dev-jane
# Delete completely when you are done:
$ sls remove --stage dev-jane

©️ Copyright

Copyright puntonim (https://github.com/puntonim). No License.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors