n26-puppeteer-scraper
Version:
n26 scraper: a tool to download account informations and transactions archive
134 lines (111 loc) • 4.7 kB
JavaScript
const megaScraper = require('mega-scraper')
const pRetry = require('p-retry')
const sendTelegramMessage = require('./send-telegram-message')
const debug = require('debug')
const log = debug('n26:login')
module.exports = n26login
async function n26login (credentials = {}, { browser, page, headless = false, loadMembership = true } = {}) {
if (!credentials) throw new WrongCredentials('missing credentials')
if (!credentials.username) throw new WrongCredentials('missing username')
if (!credentials.password) throw new WrongCredentials('missing password')
browser = browser || await megaScraper.browser.createBrowser({ headless })
log('prepared browser')
page = page || await browser.newPage()
log('prepared page')
const loggedIn = await isLoggedIn(page)
log({ loggedIn })
if (loggedIn) {
log('navigating to https://app.n26.com/account')
await page.goto('https://app.n26.com/account')
} else {
await sendTelegramMessage('prepare for confirming n26 login').catch(err => false && log(err.message))
await login(page, credentials)
}
let membership
if (loadMembership) {
membership = await getMembershipInfo(page, credentials)
}
return { page, membership }
}
async function isLoggedIn (page) {
log('navigating to https://app.n26.com/account')
await page.goto('https://app.n26.com/account')
const passwordFieldFound = await page.$('input[type=password]')
return !passwordFieldFound
}
async function getMembershipInfo (page, credentials) {
let membership
log('navigating to https://app.n26.com/account')
await page.goto('https://app.n26.com/account')
await pRetry(async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
const selector = '#membership'
log('looking for membership div', selector)
const membershipDiv = await page.$(selector)
if (membershipDiv) {
const text = await page.evaluate(() => document.querySelector('#membership').innerText) || ''
const lines = text.split('\n').filter(Boolean).filter(s => typeof s === 'string').map(s => s.trim())
log('found membership', lines.length)
const name = lines[lines.indexOf('Membership') + 1]
const iban = lines[lines.indexOf('IBAN:') + 1]
const bic = lines[lines.indexOf('BIC:') + 1]
const accountType = lines.find(l => /account type/gi.test(l)).replace(/account type: /gi, '').trim()
const atmWithdrawalsLine = lines.find(l => /ATM withdrawals/gi.test(l)) || ''
const atmWithdrawals = atmWithdrawalsLine.replace(/ATM withdrawals/gi, '').trim()
const markupLine = lines.find(l => /markup for all withdrawals/gi.test(l)) || ''
const markup = markupLine.replace(/markup for all withdrawals/gi, '').trim()
const worldwide = !!lines.find(l => /Worldwide/gi.test(l)) || ''
membership = { name, iban, bic, accountType, atmWithdrawals, markup, worldwide }
} else {
log('membership div not found')
throw new Error('membership div not found')
}
}, { retries: 5 }).catch(err => { false && log(err.message) })
return membership
}
async function login (page, credentials) {
await page.goto('https://app.n26.com/login')
log('navigated to page')
await page.type('input[type=email]', credentials.username)
await page.type('input[type=password]', credentials.password)
log('filled credentials page', credentials.username, '*'.repeat(credentials.password.length))
await page.click('button[type=submit]')
log('submitted form')
await pRetry(async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
const continueSelector = '#verify-login-dialog form button'
const continueButton = await page.$(continueSelector)
if (continueButton) {
log('found continue button')
continueButton.click && await continueButton.click()
log('clicked continue button')
} else {
throw new Error('continue link not found')
}
}, { retries: 10 })
log('confirm on n26 in 30s')
return new Promise((resolve, reject) => {
const start = Date.now()
const handle = setInterval(async () => {
try {
const accountsElement = await page.$('[href="/account"]')
log('checking n26 login status..')
if (accountsElement) {
clearInterval(handle)
resolve(true)
}
if (start < Date.now() - 1000 * 60 * 1) {
clearInterval(handle)
reject(new Error('timeout n26 confirmation'))
}
} catch (err) {
// log(err.message)
}
}, 1000)
})
}
function WrongCredentials (message) {
this.name = 'Wrong Credentials Error'
this.message = message
}
WrongCredentials.prototype = Error.prototype