1
0
Fork 0

Compare commits

..

7 Commits

Author SHA1 Message Date
Dustin ca8bff8fc5 chase: Handle both CSV schemata
Apparently, Chase has switched back to the CSV schema without the Card
column at the beginning.  Just in case they decide to flip-flop on that
field forever, we better try to handle both cases.
2023-11-04 17:43:55 -05:00
Dustin 45b9e64ec1 chase: Update CSV mapping
Chase added a new "Card" field to the beginning of each record in their
CSV exports.
2023-10-09 10:03:41 -05:00
Dustin 5ea5d09b30 chase: Improve navigation robustness
Chase likes to subtly change their website fairly regularly, usually by
introducing more ads or changing the location of existing widgets.
2023-10-08 09:50:15 -05:00
Dustin d1c947c549 chase: Fixes for site updates
Chase made some minor updates to their site recently which affected some
of the element locators.  The propaganda in the right-hand column of the
landing page has changed, and the Downlod Account Activity form is still
really terrible, and now behaves even more strangely.
2023-07-24 11:17:50 -05:00
Dustin 9831fa818f commerce: Handle interstitial ads
Commerce likes to occasionally inject ads and other propaganda after the
login page, before loading the account summary page.  To handle this, we
may need to specifically navigate to the account summary page after
logging in.
2023-07-24 11:15:52 -05:00
Dustin 805aa40e20 commerce: Use modal form to download CSV
The Commerce Bank website no longer allows navigating directly to
`Download.ashx`; doing so just returns a generic "we're sorry" error.
They appear to have added some CSRF protection or something that makes
this not work.  As a result, we have to go fill out the form on the
*Download Transactions* modal dialog in order to get the download to
work correctly.
2023-07-13 18:01:34 -05:00
Dustin 3b432fc7d6 ntfy: Handle non-ASCII characters in message
In order to set the message for a notification with an attachment, the
text must be specified in the `Message` request header.  Unfortunately,
HTTP header values are limited to the Latin-1 character set, so Unicode
characters cannot be included.  As of *ntfy* 2.4.0, however, the server
can decode base64-encoded headers using the RFC 2047 scheme.

To maintain compatibility with older *ntfy* servers, the `ntfy` function
will only encode message contents this way if the string cannoto be
encoded as ASCII.
2023-06-23 09:33:24 -05:00
1 changed files with 74 additions and 22 deletions

View File

@ -1,3 +1,4 @@
import base64
import copy
import datetime
import json
@ -8,6 +9,7 @@ import shlex
import shutil
import subprocess
import tempfile
import urllib.parse
from pathlib import Path
from types import TracebackType
from typing import Any, Optional, Type
@ -51,7 +53,13 @@ def ntfy(
if filename:
headers['Filename'] = filename
if message:
headers['Message'] = message.replace('\n', '\\n')
try:
message.encode("ascii")
except UnicodeEncodeError:
message = rfc2047_base64encode(message)
else:
message = message.replace('\n', '\\n')
headers['Message'] = message
r = requests.put(
url,
headers=headers,
@ -117,6 +125,13 @@ def rbw_code(
return p.stdout.rstrip('\n')
def rfc2047_base64encode(
message: str,
) -> str:
encoded = base64.b64encode(message.encode("utf-8")).decode("ascii")
return f"=?UTF-8?B?{encoded}?="
def firefly_import(csv: Path, config: dict[str, Any], token: str) -> None:
log.debug('Importing transactions from %s to Firefly III', csv)
env = {
@ -382,6 +397,10 @@ class CommerceBank:
self.page.get_by_role('button', name='Continue').click()
log.debug('Waiting for page load')
self.page.wait_for_load_state()
cur_url = urllib.parse.urlparse(self.page.url)
if cur_url.path != '/CBI/Accounts/Summary':
new_url = cur_url._replace(path='/CBI/Accounts/Summary', query='')
self.page.goto(urllib.parse.urlunparse(new_url))
log.info('Successfully logged in to Commerce Bank')
self._logged_in = True
@ -407,21 +426,29 @@ class CommerceBank:
self, from_date: datetime.date, to_date: datetime.date
) -> Path:
log.info('Downloading transactions from %s to %s', from_date, to_date)
idx = self.page.url.rstrip('/').split('/')[-1]
href = (
f'Download.ashx?Index={idx}'
f'&From={from_date}&To={to_date}'
f'&Type=csv'
'&DurationOfMonths=6'
)
log.debug('Navigating to %s', href)
datefmt = '%m/%d/%Y'
self.page.get_by_role('link', name='Download Transactions').click()
self.page.wait_for_timeout(random.randint(750, 1250))
modal = self.page.locator('#download-transactions')
input_from = modal.locator('input[data-qaid=fromDate]')
input_from.click()
self.page.keyboard.press('Control+A')
self.page.keyboard.press('Delete')
self.page.keyboard.type(from_date.strftime(datefmt))
input_to = modal.locator('input[data-qaid=toDate]')
input_to.click()
self.page.keyboard.press('Control+A')
self.page.keyboard.press('Delete')
self.page.keyboard.type(to_date.strftime(datefmt))
modal.get_by_role('button', name='Select Type').click()
self.page.get_by_text('Comma Separated').click()
with self.page.expect_download() as di:
self.page.evaluate(f'window.location.href = "{href}";')
self.page.get_by_role('button', name='Download').click()
log.debug('Waiting for download to complete')
self.page.wait_for_timeout(random.randint(1000, 3000))
path = di.value.path()
assert path
log.info('Downloaded transactions to %s', path)
modal.get_by_label('Close').click()
return path
def firefly_import(self, csv: Path, account: int, token: str) -> None:
@ -443,6 +470,7 @@ class Chase:
'skip_form': False,
'add_import_tag': True,
'roles': [
'_ignore',
'date_transaction',
'date_process',
'description',
@ -451,7 +479,16 @@ class Chase:
'amount',
'note',
],
'do_mapping': [False, False, False, True, False, False, False],
'do_mapping': [
False,
False,
False,
False,
False,
False,
False,
False,
],
'mapping': [],
'duplicate_detection_method': 'classic',
'ignore_duplicate_lines': True,
@ -541,8 +578,9 @@ class Chase:
).click()
log.debug('Waiting for page load')
self.page.wait_for_load_state()
self.page.get_by_text('Amazon Rewards points').wait_for(timeout=60000)
self.page.get_by_role('button', name='Open an account').wait_for()
self.page.get_by_role('button', name='Pay Card').wait_for(
timeout=120000
)
log.info('Successfully logged in to Chase')
self._logged_in = True
@ -551,15 +589,19 @@ class Chase:
) -> Path:
log.info('Downloading transactions from %s to %s', from_date, to_date)
fmt = '%m/%d/%Y'
href = '#/dashboard/accountDetails/downloadAccountTransactions/index'
self.page.evaluate(f'window.location.href = "{href}";')
log.debug('Waiting for page to load')
s = self.page.locator('button#select-downloadActivityOptionId')
s.wait_for()
self.page.locator('#CARD_ACCOUNTS').get_by_role(
'button', name='CREDIT CARD (...2467)'
).first.click()
fl = self.page.locator('#flyout')
fl.wait_for()
fl.get_by_role('button', name='Pay card', exact=True).wait_for()
fl.get_by_role(
'button', name='Account activity', exact=True
).wait_for()
fl.get_by_role('link', name='Show details').wait_for()
fl.get_by_role('button', name='Download Account Activity').click()
log.debug('Filling account activity download form')
self.page.locator('button#select-account-selector').click()
self.page.get_by_text('CREDIT CARD').nth(1).locator('../..').click()
s.click()
self.page.locator('#select-downloadActivityOptionId-label').click()
self.page.get_by_text('Choose a date range').nth(1).locator(
'../..'
).click()
@ -596,6 +638,16 @@ class Chase:
def firefly_import(self, csv: Path, account: int, token: str) -> None:
config = copy.deepcopy(self.IMPORT_CONFIG)
config['default_account'] = account
with csv.open('r', encoding='utf-8') as f:
headers = f.readline()
if headers.startswith('Card'):
log.debug('Detected CSV schema with Card column')
elif headers.count(',') == 6:
log.debug('Detected CSV schema without Card column')
config['roles'].pop(0)
config['do_mapping'].pop(0)
else:
raise ValueError(f'Unexpected CSV schema: {headers}')
firefly_import(csv, config, token)