"""Backend for SyncTech Backup & Restore. [SyncTech Backup & Restore](https://www.synctech.com.au/sms-backup-restore/) for Android is a free app for backing up your SMS and MMS messages. It uses an XML format as backup format, which this backend reads and converts to the standardized Message format. """ import datetime import logging from collections.abc import Iterator from pathlib import Path import bs4 from .data import MYSELF, Message logger = logging.getLogger(__name__) def is_named_number(num: str) -> str: try: int(num.removeprefix('+').replace(' ', '')) return False except ValueError: return True def normalize_phone_number(num: str) -> str: if is_named_number(num): return num num = num.replace(' ', '') if num.startswith('00'): num = '+' + num.removeprefix('00') if len(num) == 8: num = '+45' + num elif len(num) >= 10 and num[0] != '+': num = '+' + num return num def sms_soup_to_message(soup: bs4.BeautifulSoup) -> Message: # TODO: Require myself sent_at = datetime.datetime.fromtimestamp(int(soup['date']) / 1000) phone_num = normalize_phone_number(soup['address']) if is_named_number(phone_num): contact_name = phone_num phone_num = None else: contact_name = soup.get('contact_name') or phone_num if contact_name == '(Unknown)': contact_name = None if soup['type'] == '2': sender = MYSELF else: sender = contact_name or phone_num text = soup['body'] chat_id_parts = ['SMS', contact_name or phone_num] chat_id = ' '.join(p for p in chat_id_parts if p) return Message(sent_at, sender, text, chat_id=chat_id) def select_newest_file_in_dir(path: Path) -> Path: return max( (p for p in path.iterdir() if p.suffix == '.xml' and 'sms' in p.name), key=lambda p: p.stat().st_atime_ns, ) def parse_messages_in_backup_xml_file(path: Path) -> Iterator[Message]: if path.is_dir(): logger.info('%s is a dir. Finding newest backup in directory', path) path = select_newest_file_in_dir(path) logger.info('Found: %s', path) logger.info('Parsing %s', path) with open(path) as f: soup = bs4.BeautifulSoup(f, 'lxml-xml') for sms in soup.find_all('sms'): yield sms_soup_to_message(sms) del sms