feat: NodeRED implementation

This commit is contained in:
Hugo Poissonnet 2021-12-22 17:36:33 +01:00
parent cb929774df
commit 8c816c7de7
5 changed files with 388 additions and 338 deletions

View File

@ -4,29 +4,35 @@ Automation tool for POSTING your [Netflix](https://www.netflix.com) invoice to [
## Requirements
`apt install bash curl grep jq wkhtmltopdf`
* Docker
* An instance of [Url to PDF](https://github.com/alvarcarto/url-to-pdf-api) to convert html invoice to PDF
## Usage
1. `cp .env.netflix_cookies.example .env.netflix_cookies`
1. `cp config.sh.example .env.netflix_cookies`
1. Fill the `.env.netflix_cookies` with `SecureNetflixId` and `NetflixId`.
In firefox these are in the developper tools in `Storage/Cookies`
1. Fill the `config.sh` with your email and password for Leeto
1. Change the amount of your invoice usally either `7.99`, `11.99` or
`15.99`
1. If you're not part of [Monibrand](https://www.monibrand.com) you need
to change the `LEETO_ORGANISATION_ID` and `LEETO_QUOTUM_ID` with yours
1. `$ ./last-invoice-to-leeto.sh`
1. then clean the cached files `rm *.html *.pdf`
1. `cd node_red_data`
1. `npm install`
1. `docker run -it -p 1880:1880 -v $PWD/node_red_data:/data nodered/node-red`
1. You need to provide credentials for
* `url-to-pdf-api Credentials`
* `url` example `https://url-to-pdf-api.herokuapp.com/api/render`
* `apiToken` for Url to PDF if needed
* `Netflix Credentials` Netflix
* `netflixId` get the value from cookies in a web browser (
through the inspector )
* `secureNetflixId` get the value from cookies in a web browser (
through the inspector )
* `Leeto Credentials` [Leeto](https://app.leeto.co/)
* `login` is an email
* `password`
## TODO
* Put this in an automation cron like Zappier or IFTTT or NodeRED
* Add an automation cron
* Deploy to cloud something
* Multi-user (webui maybe ?)
* Better handling error
* Netflix login (can be a pain in the ass)
* Trigger on the good invoice date of Netflix once a month
* Better error handling
* Handle `La somme demandée ne peut être supérieure à ses droits (somme restante: 4.01)`
* Implement other invoice types like Spotify

View File

@ -1,38 +0,0 @@
#!/bin/bash
set -e
source ./config.sh
# Get the Netflix Billing Page
[[ -f "BillingActivity.html" ]] || curl -v 'https://www.netflix.com/BillingActivity'\
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'\
--compressed \
--config .env.browser_fingerprint\
--config .env.netflix_cookies > BillingActivity.html
# Download the last invoice
LAST_INVOICE_URL=$(cat BillingActivity.html | grep -o '/invoice/print/[^"]*' | head -1)
[[ -f "LastInvoice.html" ]] || curl -v --compressed --config .env.netflix_cookies "$NETFLIX_BASE_URL$LAST_INVOICE_URL" > LastInvoice.html
# Convert to pdf
[[ -f "LastInvoice.pdf" ]] || wkhtmltopdf LastInvoice.html LastInvoice.pdf
# Leeto login
[[ -f "leeto_auth_payload.json" ]] || curl -v 'https://api.leeto.co/api/v2/users/sign_in?locale=fr'\
-H 'Accept: application/json, text/plain, */*'\
--config .env.browser_fingerprint\
--compressed -H 'Content-Type: application/json'\
--data-raw '{"user":{"email":"'"$LEETO_EMAIL"'","password":"'"$LEETO_PASSWORD"'"}}' > leeto_auth_payload.json
LEETO_BEARER_TOKEN=$(cat leeto_auth_payload.json | jq --raw-output '.token')
LEETO_USER_ID=$(cat leeto_auth_payload.json | jq --raw-output '.id')
curl -v "https://api.leeto.co/api/v2/organisations/$LEETO_ORGANISATION_ID/reimbursement_requests?locale=fr"\
-H 'Accept: application/json, text/plain, */*'\
--compressed \
--header "Authorization: Bearer $LEETO_BEARER_TOKEN"\
--config .env.browser_fingerprint\
--form "reimbursement_request[user_id]=$LEETO_USER_ID"\
--form "reimbursement_request[grantable_id]=$LEETO_USER_ID"\
--form "reimbursement_request[grantable_type]=User"\
--form "reimbursement_request[amount]=$LEETO_AMOUNT"\
--form "reimbursement_request[quotum_id]=$LEETO_QUOTUM_ID"\
--form "reimbursement_request[receipts_attributes][][image]=@LastInvoice.pdf"

View File

@ -45,6 +45,45 @@
"env": [],
"color": "#DDAA99"
},
{
"id": "6d4fb2bd.73cc2c",
"type": "function",
"z": "fdd2168a.835ab8",
"name": "HTTP Headers for Netflix",
"func": "msg.headers = {};\nmsg.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0';\nmsg.headers['Accept-Language'] = 'fr-FR,fr;q=0.5';\nmsg.headers['Cookie'] = [\n 'SecureNetflixId=' + msg.secureNetflixId,\n 'NetflixId=' + msg.netflixId,\n].join(';');\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"x": 410,
"y": 80,
"wires": [
[]
]
},
{
"id": "bf25f228.3c6a5",
"type": "credentials",
"z": "fdd2168a.835ab8",
"name": "Netflix Credentials",
"props": [
{
"value": "netflixId",
"type": "msg"
},
{
"value": "secureNetflixId",
"type": "msg"
}
],
"x": 190,
"y": 80,
"wires": [
[
"6d4fb2bd.73cc2c"
]
]
},
{
"id": "f120e3b1.f67b38",
"type": "inject",
@ -96,45 +135,6 @@
],
"info": "Get invoice page from Netflix"
},
{
"id": "6d4fb2bd.73cc2c",
"type": "function",
"z": "fdd2168a.835ab8",
"name": "HTTP Headers for Netflix",
"func": "msg.headers = {};\nmsg.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0';\nmsg.headers['Accept-Language'] = 'fr-FR,fr;q=0.5';\nmsg.headers['Cookie'] = [\n 'SecureNetflixId=' + msg.secureNetflixId,\n 'NetflixId=' + msg.netflixId,\n].join(';');\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"x": 410,
"y": 80,
"wires": [
[]
]
},
{
"id": "bf25f228.3c6a5",
"type": "credentials",
"z": "fdd2168a.835ab8",
"name": "Netflix Credentials",
"props": [
{
"value": "netflixId",
"type": "msg"
},
{
"value": "secureNetflixId",
"type": "msg"
}
],
"x": 190,
"y": 80,
"wires": [
[
"6d4fb2bd.73cc2c"
]
]
},
{
"id": "7ddcb19f.58034",
"type": "http request",
@ -258,11 +258,253 @@
"type": "link out",
"z": "e79fd0e1.60f55",
"name": "Last Netflix Invoice as PDF",
"links": [],
"x": 455,
"y": 580,
"mode": "link",
"links": [
"848ba398.23bef8"
],
"x": 635,
"y": 540,
"wires": []
},
{
"id": "174093e7.8794e4",
"type": "html",
"z": "e79fd0e1.60f55",
"name": "",
"property": "payload",
"outproperty": "payload",
"tag": ".invoiceFooter dd",
"ret": "text",
"as": "single",
"x": 170,
"y": 400,
"wires": [
[
"5f5a3dc9.eab71c"
]
]
},
{
"id": "d2bf2b71.93482",
"type": "change",
"z": "e79fd0e1.60f55",
"name": "Move as payload.file",
"rules": [
{
"t": "move",
"p": "payload",
"pt": "msg",
"to": "payload.file",
"tot": "msg"
},
{
"t": "set",
"p": "payload.filename",
"pt": "msg",
"to": "netflix-invoice.pdf",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 660,
"y": 400,
"wires": [
[
"97cde9aaf371b9ac"
]
]
},
{
"id": "73c55749.b6bf7",
"type": "debug",
"z": "e79fd0e1.60f55",
"name": "Tap payload",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 690,
"y": 480,
"wires": []
},
{
"id": "8e61a051.3430c8",
"type": "debug",
"z": "e79fd0e1.60f55",
"d": true,
"name": "Last invoice url",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "url",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 800,
"y": 100,
"wires": []
},
{
"id": "5f5a3dc9.eab71c",
"type": "change",
"z": "e79fd0e1.60f55",
"name": "parse amount",
"rules": [
{
"t": "move",
"p": "payload[0]",
"pt": "msg",
"to": "textAmount",
"tot": "msg"
},
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "{}",
"tot": "json"
},
{
"t": "set",
"p": "payload.amount",
"pt": "msg",
"to": "$number($replace($match(textAmount, /[0-9\\,]+/, 1).match, ',', '.'))",
"tot": "jsonata"
},
{
"t": "set",
"p": "payload.name",
"pt": "msg",
"to": "Netflix",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 360,
"y": 400,
"wires": [
[
"97cde9aaf371b9ac"
]
]
},
{
"id": "643c6d17.cec784",
"type": "function",
"z": "e79fd0e1.60f55",
"name": "Cleaning payload",
"func": "return {payload: msg.payload};",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"x": 470,
"y": 500,
"wires": [
[
"73c55749.b6bf7",
"4c6af2dc.2504bc"
]
]
},
{
"id": "2d46da6b.8dc806",
"type": "subflow:fdd2168a.835ab8",
"z": "e79fd0e1.60f55",
"name": "Netflix HTTP Auth",
"env": [],
"x": 170,
"y": 100,
"wires": [
[
"a9e0512d.6cdd7"
]
]
},
{
"id": "797b306d.21639",
"type": "subflow:fdd2168a.835ab8",
"z": "e79fd0e1.60f55",
"name": "Netflix HTTP Auth",
"env": [],
"x": 170,
"y": 160,
"wires": [
[
"7ddcb19f.58034"
]
]
},
{
"id": "41d7704e.4f4a48",
"type": "change",
"z": "e79fd0e1.60f55",
"name": "Extract last invoice url",
"rules": [
{
"t": "set",
"p": "url",
"pt": "msg",
"to": "$join([\t 'https://www.netflix.com',\t $lookup($match(payload, /\\/invoice\\/print\\/[^\"]*/), 'match')[0]\t])",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 540,
"y": 100,
"wires": [
[
"8e61a051.3430c8",
"797b306d.21639"
]
]
},
{
"id": "97cde9aaf371b9ac",
"type": "join",
"z": "e79fd0e1.60f55",
"name": "",
"mode": "custom",
"build": "merged",
"property": "payload",
"propertyType": "msg",
"key": "topic",
"joiner": "\\n",
"joinerType": "str",
"accumulate": false,
"timeout": "",
"count": "3",
"reduceRight": false,
"reduceExp": "",
"reduceInit": "",
"reduceInitType": "",
"reduceFixup": "",
"x": 260,
"y": 520,
"wires": [
[
"643c6d17.cec784"
]
],
"info": "NOTE : How the fuck this join require 3 as number of messages and not 2 as expected."
},
{
"id": "debc0868.c889a",
"type": "inject",
@ -382,7 +624,7 @@
"proxy": "",
"authType": "",
"x": 650,
"y": 420,
"y": 440,
"wires": [
[
"337393c6.282844"
@ -394,16 +636,18 @@
"type": "function",
"z": "34256184.7e2b8e",
"name": "Leeto payload for reimbursement",
"func": "msg.headers = {};\nmsg.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0';\nmsg.headers['Content-Type'] = 'multipart/form-data';\nmsg.headers['Accept'] = 'application/json, text/plain, */*';\nmsg.headers['Accept-Language'] = 'fr-FR,fr;q=0.5';\nmsg.headers['Authorization'] = 'Bearer ' + msg.leetoUser.token;\nmsg.payload = {\n 'reimbursement_request[user_id]': msg.leetoUser.id,\n 'reimbursement_request[grantable_id]': msg.leetoUser.id,\n 'reimbursement_request[grantable_type]': 'User',\n 'reimbursement_request[amount]': msg.invoice.amount,\n 'reimbursement_request[quotum_id]': msg.benefit.data.period.quota[0].id,\n 'reimbursement_request[receipts_attributes][][image]': {\n value: msg.invoice.file,\n options: {\n filename: msg.invoice.filename,\n contentType: msg.invoice.contentType,\n }\n }\n}\nreturn msg;",
"func": "msg.headers = {};\nmsg.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0';\nmsg.headers['Content-Type'] = 'multipart/form-data';\nmsg.headers['Accept'] = 'application/json, text/plain, */*';\nmsg.headers['Accept-Language'] = 'fr-FR,fr;q=0.5';\nmsg.headers['Authorization'] = 'Bearer ' + msg.leetoUser.token;\n\nmsg.payload = {\n 'reimbursement_request[user_id]': msg.leetoUser.id,\n 'reimbursement_request[grantable_id]': msg.leetoUser.id,\n 'reimbursement_request[grantable_type]': 'User',\n 'reimbursement_request[quotum_id]': msg.benefit.data[0].quotumId,\n 'reimbursement_request[purchases][][generate_certificate_of_honour]': false,\n // 'reimbursement_request[purchases][][comments_attributes][][user_id]': msg.leetoUser.id,\n // 'reimbursement_request[purchases][][comments_attributes][][organisation_id]': msg.leetoUser.organisation.id,\n // 'reimbursement_request[purchases][][comments_attributes][][body]': 'This a comment',\n 'reimbursement_request[purchases][][purchase_name]': msg.invoice.name,\n 'reimbursement_request[purchases][][amount]': (msg.benefit.data[0].remainingAmount < msg.invoice.amount) ? msg.benefit.data[0].remainingAmount : msg.invoice.amount,\n 'reimbursement_request[purchases][][requested_amount]': msg.invoice.amount,\n 'reimbursement_request[purchases][][receipts_attributes][][image]': {\n value: msg.invoice.file,\n options: {\n filename: msg.invoice.filename,\n contentType: msg.invoice.contentType,\n }\n }\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 340,
"y": 420,
"y": 440,
"wires": [
[
"f5429dca.a2783"
"f5429dca.a2783",
"a689e897.689f68"
]
]
},
@ -421,7 +665,7 @@
"statusVal": "",
"statusType": "auto",
"x": 910,
"y": 420,
"y": 440,
"wires": []
},
{
@ -472,7 +716,7 @@
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "https://api.leeto.co/api/v2/organisations/{{leetoUser.organisationId}}/users/{{leetoUser.id}}/benefits/{{leetoUser.authorizedBenefitIds.0}}?locale=fr",
"template": "https://api.leeto.co/api/v2/organisations/{{leetoUser.organisationId}}/users/{{leetoUser.id}}/quotum_usages?filter[by_state]=active&locale=fr",
"output": "str",
"x": 120,
"y": 320,
@ -491,10 +735,10 @@
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "https://api.leeto.co/api/v2/organisations/{{leetoUser.organisationId}}/reimbursement_requests?locale=fr",
"template": "https://api.leeto.co/api/v2/organisations/{{leetoUser.organisationId}}/reimbursement_requests/bulk_create?locale=fr",
"output": "str",
"x": 120,
"y": 420,
"y": 440,
"wires": [
[
"5571d225.8e12dc"
@ -506,7 +750,9 @@
"type": "link in",
"z": "34256184.7e2b8e",
"name": "Trigged from Netflix Last Invoice",
"links": [],
"links": [
"4c6af2dc.2504bc"
],
"x": 475,
"y": 120,
"wires": [
@ -570,236 +816,6 @@
]
]
},
{
"id": "174093e7.8794e4",
"type": "html",
"z": "e79fd0e1.60f55",
"name": "",
"property": "payload",
"outproperty": "payload",
"tag": ".invoiceFooter dd",
"ret": "text",
"as": "multi",
"x": 170,
"y": 400,
"wires": [
[
"5f5a3dc9.eab71c"
]
]
},
{
"id": "6af264f9.2f99dc",
"type": "join",
"z": "e79fd0e1.60f55",
"name": "",
"mode": "custom",
"build": "merged",
"property": "payload",
"propertyType": "msg",
"key": "topic",
"joiner": "\\n",
"joinerType": "str",
"accumulate": false,
"timeout": "",
"count": "2",
"reduceRight": false,
"reduceExp": "",
"reduceInit": "",
"reduceInitType": "num",
"reduceFixup": "",
"x": 110,
"y": 540,
"wires": [
[
"643c6d17.cec784"
]
]
},
{
"id": "d2bf2b71.93482",
"type": "change",
"z": "e79fd0e1.60f55",
"name": "Move as payload.file",
"rules": [
{
"t": "move",
"p": "payload",
"pt": "msg",
"to": "payload.file",
"tot": "msg"
},
{
"t": "set",
"p": "payload.filename",
"pt": "msg",
"to": "netflix-invoice.pdf",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 660,
"y": 400,
"wires": [
[
"6af264f9.2f99dc"
]
]
},
{
"id": "73c55749.b6bf7",
"type": "debug",
"z": "e79fd0e1.60f55",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 490,
"y": 520,
"wires": []
},
{
"id": "8e61a051.3430c8",
"type": "debug",
"z": "e79fd0e1.60f55",
"name": "Last invoice url",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "url",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 800,
"y": 100,
"wires": []
},
{
"id": "5f5a3dc9.eab71c",
"type": "change",
"z": "e79fd0e1.60f55",
"name": "parse amount",
"rules": [
{
"t": "move",
"p": "payload",
"pt": "msg",
"to": "textAmount",
"tot": "msg"
},
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "{}",
"tot": "json"
},
{
"t": "set",
"p": "payload.amount",
"pt": "msg",
"to": "$number($replace($match(textAmount, /[0-9\\,]+/, 1).match, ',', '.'))",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 360,
"y": 400,
"wires": [
[
"6af264f9.2f99dc"
]
]
},
{
"id": "643c6d17.cec784",
"type": "function",
"z": "e79fd0e1.60f55",
"name": "Cleaning payload",
"func": "return {payload: msg.payload};",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"x": 290,
"y": 540,
"wires": [
[
"73c55749.b6bf7",
"4c6af2dc.2504bc"
]
]
},
{
"id": "2d46da6b.8dc806",
"type": "subflow:fdd2168a.835ab8",
"z": "e79fd0e1.60f55",
"name": "Netflix HTTP Auth",
"env": [],
"x": 170,
"y": 100,
"wires": [
[
"a9e0512d.6cdd7"
]
]
},
{
"id": "797b306d.21639",
"type": "subflow:fdd2168a.835ab8",
"z": "e79fd0e1.60f55",
"name": "Netflix HTTP Auth",
"env": [],
"x": 170,
"y": 160,
"wires": [
[
"7ddcb19f.58034"
]
]
},
{
"id": "41d7704e.4f4a48",
"type": "change",
"z": "e79fd0e1.60f55",
"name": "Extract last invoice url",
"rules": [
{
"t": "set",
"p": "url",
"pt": "msg",
"to": "$join([\t 'https://www.netflix.com',\t $lookup($match(payload, /\\/invoice\\/print\\/[^\"]*/), 'match')[0]\t])",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 540,
"y": 100,
"wires": [
[
"8e61a051.3430c8",
"797b306d.21639"
]
]
},
{
"id": "8a75e7ed.ee7658",
"type": "change",
@ -866,6 +882,41 @@
"pt": "msg",
"to": "benefit",
"tot": "msg"
},
{
"t": "set",
"p": "parts.id",
"pt": "msg",
"to": "leeto-infos",
"tot": "str"
},
{
"t": "set",
"p": "parts.index",
"pt": "msg",
"to": "0",
"tot": "str"
},
{
"t": "set",
"p": "parts.count",
"pt": "msg",
"to": "2",
"tot": "str"
},
{
"t": "set",
"p": "parts.type",
"pt": "msg",
"to": "object",
"tot": "str"
},
{
"t": "set",
"p": "parts.key",
"pt": "msg",
"to": "benefit",
"tot": "str"
}
],
"action": "",
@ -880,5 +931,22 @@
"17d03aa5.9a232d"
]
]
},
{
"id": "a689e897.689f68",
"type": "debug",
"z": "34256184.7e2b8e",
"name": "Payload for leeto",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 770,
"y": 580,
"wires": []
}
]

View File

@ -1,8 +1,22 @@
{
"name": "node-red-project",
"name": "auto-invoice-leeto",
"version": "0.0.1",
"lockfileVersion": 1,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "auto-invoice-leeto",
"version": "0.0.1",
"dependencies": {
"node-red-contrib-credentials": "^0.2.1"
}
},
"node_modules/node-red-contrib-credentials": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-red-contrib-credentials/-/node-red-contrib-credentials-0.2.1.tgz",
"integrity": "sha512-fn82zQ2Tn/tsj/i634s+Oso2hEgMWC61lMU315cfwplURW+UIeZ5+dFCWOPYs4qlI2VUPW5qfzHPHg4OVjV/UQ=="
}
},
"dependencies": {
"node-red-contrib-credentials": {
"version": "0.2.1",

View File

@ -1,6 +1,6 @@
{
"name": "node-red-project",
"description": "A Node-RED Project",
"name": "auto-invoice-leeto",
"description": "A Node-RED Project to send Netflix invoice to leeto automatically",
"version": "0.0.1",
"private": true,
"dependencies": {