Ein Telegram-Bot, um das Hinzufügen von Transaktionen zu YNAB zu erleichtern

Veröffentlicht am 22.07.2022 von Dirk van der Laarse

Wir verwenden das beliebte Budgetierungstool YNAB und befolgen gerne deren 4 RegelnIn einem neuen Fenster öffnen.

Sie unterstützen den Direktimport von Banken, konzentrieren sich jedoch, wie viele US-amerikanische Unternehmen, nur auf den US-MarktIn einem neuen Fenster öffnen.

Wir haben auch ein Konto bei Investec, einer der wenigen lokalen Banken, die Programmable BankingIn einem neuen Fenster öffnen anbieten, mit anderen Worten, eine API. Sie ermöglichen es auch, vor und nach Kartentransaktionen JavaScript-Snippets auszuführen.

Lasst uns einen Telegram-Bot bauen, der eine Nachricht in unserem Familien-Telegram-Chat sendet, sobald eine Kartentransaktion durchgeführt wird! Wir können die Nachricht interaktiv gestalten, sodass jeder:

  • Den Empfänger auswählen
  • Die Transaktion kategorisieren
  • Eine Notiz/Beschreibung hinzufügen

Der Betrag und das Datum können automatisch geparst werden - keine Tippfehler bei den Beträgen mehr, die am Monatsende bei der Abstimmung in YNAB behoben werden müssen!


Hier sind einige hilfreiche Links:


Investec Programmable Banking

Der erste Schritt ist das Erstellen eines Proof-of-Concept-Skripts.

Erstellen Sie main.js im Investec Programmable Banking:

const TransactionResult = {
    Declined: 0,
    Success: 1,
    Reversed: 2
}

// Wir verwenden eine Kombination aus drei Emojis, um Transaktionen zu identifizieren und ihre Antworten zu verknüpfen.
const getRandomEmoji = (cat, sub) => {
    const emojis = {
        'Tiere & Natur': {
            'Säugetiere':['🐵','🐒','🦍','🦧','🐶','🐕','🦮','🐕‍🦺','🐩','🐺','🦊','🦝','🐱','🐈','🦁','🐯','🐅','🐆','🐴','🐎','🦄','🦓','🦌','🦬','🐮','🐂','🐃','🐄','🐷','🐖','🐗','🐽','🐏','🐑','🐐','🐪','🐫','🦙','🦒','🐘','🦣','🦏','🦛','🐭','🐁','🐀','🐹','🐰','🐇','🐿','🦫','🦔','🦇','🐻','🐻‍❄️','🐨','🐼','🦥','🦦','🦨','🦘','🦡','🐾'],
            'Vögel':['🦃','🐔','🐓','🐣','🐤','🐥','🐦','🐧','🕊','🦅','🦆','🦢','🦉','🦤','🪶','🦩','🦚','🦜'],
            'Amphibien':['🐸'],
            'Reptilien':['🐊','🐢','🦎','🐍','🐲','🐉','🦕','🦖'],
            'Meereslebewesen':['🐳','🐋','🐬','🦭','🐟','🐠','🐡','🦈','🐙','🐚'],
            'Insekten':['🐌','🦋','🐛','🐜','🐝','🪲','🐞','🦗','🪳','🕷','🕸','🦂','🦟','🪰','🪱','🦠'],
            'Pflanzen & Blumen':['💐','🌸','💮','🏵','🌹','🥀','🌺','🌻','🌼','🌷'],
            'Pflanzen (sonstige)':['🌱','🪴','🌲','🌳','🌴','🌵','🌾','🌿','☘','🍀','🍁','🍂','🍃']
        },
        'Essen & Trinken': {
            'Obst':['🍇','🍈','🍉','🍊','🍋','🍌','🍍','🥭','🍎','🍏','🍐','🍑','🍒','🍓','🫐','🥝','🍅','🫒','🥥'],
            'Gemüse':['🥑','🍆','🥔','🥕','🌽','🌶','🫑','🥒','🥬','🥦','🧄','🧅','🍄','🥜','🌰'],
            'Vorbereitetes Essen':['🍞','🥐','🥖','🫓','🥨','🥯','🥞','🧇','🧀','🍖','🍗','🥩','🥓','🍔','🍟','🍕','🌭','🥪','🌮','🌯','🫔','🥙','🧆','🥚','🍳','🥘','🍲','🫕','🥣','🥗','🍿','🧈','🧂','🥫'],
            'Asiatisches Essen':['🍱','🍘','🍙','🍚','🍛','🍜','🍝','🍠','🍢','🍣','🍤','🍥','🥮','🍡','🥟','🥠','🥡'],
            'Meeresfrüchte':['🦀','🦞','🦐','🦑','🦪'],
            'Süßigkeiten':['🍦','🍧','🍨','🍩','🍪','🎂','🍰','🧁','🥧','🍫','🍬','🍭','🍮','🍯'],
            'Getränke':['🍼','🥛','☕','🫖','🍵','🍶','🍾','🍷','🍸','🍹','🍺','🍻','🥂','🥃','🥤','🧋','🧃','🧉','🧊'],
            'Geschirr':['🥢','🍽','🍴','🥄','🔪','🏺']
        }
    }
    const random = (array) => array[~~(Math.random() * array.length)]

    if(!emojis[cat])
        cat = random(Object.keys(emojis))

    if(!emojis[cat][sub])
        sub = random(Object.keys(emojis[cat]))

    return random(emojis[cat][sub])

}

async function notifyOnTelegram(authorization, event) {
    const telegramURL = `https://api.telegram.org/bot${process.env.telegramBotToken}/sendMessage`;

    const uppercaseCurrency = authorization.currencyCode.toUpperCase();
    const transactionId = getRandomEmoji() + getRandomEmoji() + getRandomEmoji()
    const userText = "[@<Card User Here>](tg://user?id=123456789)"
    let amount = investec.helpers.format.decimal(authorization.centsAmount / 100, 100);
    amount = amount.replace(".", "\\.");

    let messageBody;
    switch(event) {
    case TransactionResult.Declined:
        messageBody = 
            [
                `${userText}, deine Transaktion wurde abgelehnt: `,
                `${uppercaseCurrency} *${amount}* `,
                `an _${authorization.merchant.name}_ `,
                `\n||_\\(${authorization.reference}\\)_||`
            ].join('')
        break;
    case TransactionResult.Success:
        messageBody = 
            [
                `${userText} `,
                `hat ${uppercaseCurrency} *${amount}* `,
                `an _${authorization.merchant.name}_ gezahlt`,
                `\n||_\\(${authorization.reference}\\)_||`,
                `\n${transactionId}`,
                `\n _Antworte, um eine Notiz/Beschreibung hinzuzufügen_`
            ].join('')
        break;
    case TransactionResult.Reversed:
        messageBody = 
            [
                `${userText}, deine Transaktion `,
                `wurde rückgängig gemacht: ${uppercaseCurrency} *${amount}* `,
                `an _${authorization.merchant.name}_ `,
                `\n||_\\(${authorization.reference}\\)_||`
            ].join('')
        break;
    default:
        messageBody = `${userText} Unbekannter Fehler`;
    }

    const raw = JSON.stringify({
        "chat_id": process.env.telegramGroupChatID,
        "text": messageBody,
        "parse_mode": "MarkdownV2",
        "disable_notification": false,
        "reply_markup": 
            {


                "inline_keyboard": [
                    [
                        {
                            "text": "Empfänger",
                            "switch_inline_query_current_chat": `Empfänger (${transactionId}):`
                        },
                        {
                            "text": "Kategorie",
                            "switch_inline_query_current_chat": `Kategorie (${transactionId}):`
                        }
                    ]
                ]
            }
    });

    const requestOptions = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: raw,
        redirect: 'follow'
    };

    const response = await fetch(telegramURL, requestOptions);
};


// Diese Funktion wird vor einer Transaktion ausgeführt.
const beforeTransaction = async (authorization) => {
    console.log(authorization);
    return true // Die Transaktion autorisieren
};

// Diese Funktion wird nach einer erfolgreichen Transaktion ausgeführt.
const afterTransaction = async (transaction) => {
  await notifyOnTelegram(transaction, TransactionResult.Success);
  console.log(transaction);
};
// Diese Funktion wird ausgeführt, nachdem eine Transaktion abgelehnt wurde.
const afterDecline = async (transaction) => {
  await notifyOnTelegram(transaction, TransactionResult.Declined);
  console.log(transaction);
};

// Diese Funktion wird ausgeführt, nachdem eine Transaktion rückgängig gemacht wurde.
const afterReversal = async (transaction) => {
  await notifyOnTelegram(transaction, TransactionResult.Reversed);
  console.log(transaction);
};

Erstellen Sie eine env.json im Investec Programmable Banking:

{
    "telegramGroupChatID": "-12312313",
    "telegramBotToken": "123:ABC-DEF"
}

Den Telegram-Bot auf Fly.io bereitstellen

Siehe das Code-Repository hier: github.com/dvdl16/ynab_data_getterIn einem neuen Fenster öffnen

Erforderliche Umgebungsvariablen
# YNAB
APP_YNAB_API_TOKEN = FGHIJK
APP_YNAB_BUDGET_ID = 8cd98e2b-ecf6-46d7-ac63-b597687213f4
APP_YNAB_ACCOUNT_ID = 224b38b5-d5e6-4d18-97eb-ea09ebae19c4
APP_UPTIME_HEARTBEAT_URL = https://kuma.app.somewhere/api/push/1a2b3c4d5e

# Telegram
APP_TELEGRAM_BOT_TOKEN = 12345:ABCDE
Erstellen Sie die App auf Fly.ioIn einem neuen Fenster öffnen

Im Stammverzeichnis mit main.py ausführen:

flyctl apps create telegram-bot
Erforderliche Secrets setzen

Setzen Sie die erforderlichen Umgebungsvariablen als Fly.io Secrets

flyctl secrets set -a telegram-bot APP_YNAB_API_TOKEN="[APP_YNAB_API_TOKEN hier]"
flyctl secrets set -a telegram-bot APP_YNAB_BUDGET_ID="[APP_YNAB_BUDGET_ID hier]"
flyctl secrets set -a telegram-bot APP_YNAB_ACCOUNT_ID="[APP_YNAB_ACCOUNT_ID hier]"
flyctl secrets set -a telegram-bot APP_TELEGRAM_BOT_TOKEN="[APP_TELEGRAM_BOT_TOKEN hier]"
flyctl secrets set -a telegram-bot APP_UPTIME_HEARTBEAT_URL="[APP_UPTIME_HEARTBEAT_URL hier]"
Fly.io Konfigurationsdatei überprüfen

Dann können wir eine Fly.io Konfigurationsdatei erstellen. Siehe fly.toml für weitere Details.

Anwendung bereitstellen

Nach all diesen Schritten sollten Sie in der Lage sein, Ihre Anwendung mit dem nächsten Befehl bereitzustellen:

flyctl deploy

Dieser Befehl baut und stellt automatisch Ihren Docker-Container mit dem Telegram-Bot bereit und startet ihn anschließend in der Cloud.

Endergebnis

telegram

Zu erledigen

  • Codequalität von POC ändern
  • Empfänger automatisch aus der Nachricht parsen
  • Möglichkeit hinzufügen, einen neuen Empfänger hinzuzufügen
  • Party-GIF für alle eingehenden Transaktionen senden
  • Bereitstellungspipeline für den Programmable Card Code bei Investec hinzufügen
Mitwirkende: Dirk van der Laarse