Skip to content

Instantly share code, notes, and snippets.

@J-Swift
Last active March 14, 2020 18:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save J-Swift/6fc31fd82234abf147687132ee7062fa to your computer and use it in GitHub Desktop.
Save J-Swift/6fc31fd82234abf147687132ee7062fa to your computer and use it in GitHub Desktop.
Puppeteer - get recent BBT transactions
const chromium = require('chrome-aws-lambda');
const chalk = require('chalk');
const USERNAME = process.env.USERNAME;
const PASSWORD = process.env.PASSWORD;
const DEFAULT_TIMEOUT = 5000;
const hl_info = chalk.bold.white;
const info = chalk.gray;
const positive = chalk.green;
const negative = chalk.red;
/**
* @typedef {import('puppeteer-core').Page} Page
*/
/**
* @param {number} length
* @param {string} str
*/
const leftPad = (length, str) => {
const needed = Math.max(0, (length - str.length));
let prefix = '';
for (let i = 0; i < needed; i++) {
prefix += ' ';
}
return `${prefix}${str}`;
};
/**
* @param {string} questionText
* @returns {string}
*/
const getAnswerForQuestion = (questionText) => {
if (questionText.match(/first car/i)) {
return 'not my real car';
}
if (questionText.match(/elementary school/i)) {
return 'not my real school';
}
throw `Unsupported question [${questionText}]`;
};
/**
* @param {Page} page
*/
const checkForSecurityQuestions = async (page) => {
console.log('-> Checking for security questions');
const questionSelector = '#security-verify label';
const answerSelector = '#security-verify input';
const rememberRadioSelector = '#remember-browser';
const submitBtnSelector = 'button.button.primary';
const questionText = await page.$eval(questionSelector, it => it.innerText).catch(_ => {
return null;
});
if (!questionText) {
console.log('-> Skipping... no security questions detected');
return;
}
await page.waitForSelector(answerSelector);
await page.type(answerSelector, getAnswerForQuestion(questionText));
await page.waitForSelector(rememberRadioSelector);
await page.click(rememberRadioSelector);
await page.waitForSelector(submitBtnSelector).then(it => it.click());
await page.waitForNavigation();
};
/**
* @param {Page} page
*/
const tryLoginMobile = async (page) => {
console.log('-> Mobile login');
const formSelector = 'form[id=login-form-1]';
const usernameSelector = `${formSelector} input[name=UserName]`;
const pwSelector = `${formSelector} input[name=Password]`;
const submitSelector = `${formSelector} button[type=submit]`;
await page.$$eval('button', els => {
const loginBtn = els.find(it => it.innerText === 'Login')
if (!loginBtn) {
throw 'No login button found!';
}
loginBtn.click();
});
await page.waitForSelector(usernameSelector);
await page.type(usernameSelector, USERNAME);
await page.waitForSelector(pwSelector);
await page.type(pwSelector, PASSWORD);
await page.waitForSelector(submitSelector).then(it => it.click());
await page.waitForNavigation();
};
/**
* @param {Page} page
*/
const tryLoginDesktop = async (page) => {
console.log('-> Desktop login');
const formSelector = 'form[id=login-form-2]';
const usernameSelector = `${formSelector} input[name=UserName]`;
const pwSelector = `${formSelector} input[name=Password]`;
const submitSelector = `${formSelector} button[type=submit]`;
await page.waitForSelector(usernameSelector);
await page.type(usernameSelector, USERNAME);
await page.waitForSelector(pwSelector);
await page.type(pwSelector, PASSWORD);
await page.waitForSelector(submitSelector).then(it => it.click());
await page.waitForNavigation();
};
/**
* @param {Page} page
*/
const gotoLoginPage = async (page) => {
console.log('-> Going to login page');
const loginUrl = 'https://www.bbt.com/online-access/online-banking.html'
await page.goto(loginUrl);
};
/**
* @param {Page} page
* @returns {Promise<string>}
*/
const getAccountBalance = async (page) => {
console.log('-> Get account balance');
const elSelector = 'div.acct-balance';
return await page.waitForSelector(elSelector)
.then(_ => page.$eval(elSelector, el => el.innerText));
};
/**
* @param {Page} page
*/
const getRecentTransactions = async (page) => {
console.log('-> Get recent transations');
const tableSelector = 'tbody#resultTransList';
return await page.waitForSelector(tableSelector)
.then(section => section.$$eval('tr', rows => {
/**
* @param {HTMLTableDataCellElement} cell
*/
const parseCell = (cell) => {
const text = cell.innerText;
return text.split("\n")[1];
};
return rows.map(row => {
const cells = row.querySelectorAll('td');
if (cells.length != 5) {
throw `Expected there to be 5 cells but there were [${cells.length}]`;
}
return {
date: parseCell(cells[0]),
desc: parseCell(cells[1]),
amount: parseCell(cells[3]),
};
});
}));
};
const main = async (page) => {
await gotoLoginPage(page)
.then(async () => await tryLoginDesktop(page))
.catch(async (_) => await tryLoginMobile(page));
await checkForSecurityQuestions(page);
const balance = await getAccountBalance(page);
const trans = await getRecentTransactions(page);
const moneyWidth = 11;
console.log(hl_info(`Balance:\t${leftPad(moneyWidth, balance)}`));
console.log('');
console.log(hl_info('Recent Transactions:'));
trans.forEach(it => {
const moneyColor = it.amount[0] == "-" ? negative : positive;
console.log(hl_info(`${it.date}\t`) + moneyColor(leftPad(moneyWidth, it.amount)));
console.log(info(it.desc));
});
};
(async () => {
const browser = await chromium.puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath,
headless: chromium.headless,
});
const page = await browser.newPage();
page.setDefaultTimeout(DEFAULT_TIMEOUT);
await main(page)
.catch(async (err) => {
console.log('--------------------------------------------------------------------------------');
console.error('ERROR:');
console.error(err);
await page.screenshot({ path: 'failure.png' });
console.log('');
console.error('You can see the failed page at [failure.png]');
})
.finally(() => browser.close());
})();
{
"name": "bbt_transaction_fetcher",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"puppeteer-core": "^2.0.0"
},
"devDependencies": {
"chalk": "^3.0.0",
"puppeteer": "^2.0.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
#!/usr/bin/env bash
set -euo pipefail
readonly keychain_name="${1:-}"
if [ -z "${keychain_name}" ]; then
echo 'ERROR: must provide a keychain name!'
exit 1
fi
if ! security find-generic-password -s "${keychain_name}" >/dev/null 2>&1; then
echo "ERROR: keychain entry not found for [${keychain_name}]"
exit 1
fi
get_username_from_keychain() {
security find-generic-password -s "${keychain_name}" | grep acct | grep -Eo '=.+' | grep -Eo '"([^"]+)"' | sed 's/"//g'
}
get_password_from_keychain() {
security find-generic-password -s "${keychain_name}" -w
}
main() {
local -r _username="$( get_username_from_keychain )"
local -r _password="$( get_password_from_keychain )"
USERNAME="${_username}" PASSWORD="${_password}" node index.js
}
main
@J-Swift
Copy link
Author

J-Swift commented Mar 14, 2020

To run, fill in your security questions/answers in getAnswerForQuestion, then either

  • add a new 'password' item to your keychain
  • run with run_it.sh {keychain-entry-name}

Or pass the credentials directly with

USERNAME={your-username} PASSWORD={your-password} node index.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment