Last active
March 14, 2020 18:55
-
-
Save J-Swift/6fc31fd82234abf147687132ee7062fa to your computer and use it in GitHub Desktop.
Puppeteer - get recent BBT transactions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To run, fill in your security questions/answers in
getAnswerForQuestion
, then eitherrun_it.sh {keychain-entry-name}
Or pass the credentials directly with
USERNAME={your-username} PASSWORD={your-password} node index.js