Files
FindMy.py/findmy/__main__.py

106 lines
3.5 KiB
Python

"""usage: python -m findmy""" # noqa: D400, D415
from __future__ import annotations
import argparse
import json
import logging
from importlib.metadata import version
from pathlib import Path
from .plist import list_accessories
def main() -> None: # noqa: D103
parser = argparse.ArgumentParser(prog="findmy", description="FindMy.py CLI tool")
parser.add_argument(
"-v",
"--version",
action="version",
version=version("FindMy"),
)
parser.add_argument(
"--log-level",
type=str,
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default="INFO",
help="Set the logging level (default: INFO)",
)
subparsers = parser.add_subparsers(dest="command", title="commands")
subparsers.required = True
decrypt_parser = subparsers.add_parser(
"decrypt",
help="""
Decrypt and print (in json) all the local FindMy accessories.
This looks through the local FindMy accessory plist files,
decrypts them using the system keychain, and prints the
decrypted JSON representation of each accessory.
eg
```
[
{
"master_key": "e01ae426431867e92d512ae1cb6c9e5bbc20a2b7d1c677d7",
"skn": "e01ae426431867e92d512ae1cb6c9e5bbc20a2b7d1c677d7",
"sks": "e01ae426431867e92d512ae1cb6c9e5bbc20a2b7d1c677d7",
"paired_at": "2020-01-08T21:26:36.177409+00:00",
"name": "Nick's MacBook Pro",
"model": "MacBookPro11,5",
"identifier": "03FF9E28-2508-425B-BD57-D738F2D2F6C0"
},
{
"master_key": "e01ae426431867e92d512ae1cb6c9e5bbc20a2b7d1c677d7",
"skn": "e01ae426431867e92d512ae1cb6c9e5bbc20a2b7d1c677d7",
"sks": "e01ae426431867e92d512ae1cb6c9e5bbc20a2b7d1c677d7",
"paired_at": "2023-10-22T20:40:39.285225+00:00",
"name": "ncmbp",
"model": "MacBookPro18,2",
"identifier": "71D276DF-A8FA-47C8-A93C-9B3B714BDFEC"
}
]
```
You can chain the output with jq or similar tools.
eg `python -m findmy decrypt | jq '.[] | select(.name == "my airtag")' > my_airtag.json`
""",
)
decrypt_parser.add_argument(
"--out-dir",
type=Path,
default=None,
help="Output directory for decrypted files. If not specified, files will not be saved to disk.", # noqa: E501
)
args = parser.parse_args()
logging.basicConfig(level=args.log_level.upper())
if args.command == "decrypt":
decrypt_all(args.out_dir)
else:
# This else block should ideally not be reached if subparsers.required is True
# and a default command isn't set, or if a command is always given.
# However, it's good practice for unexpected cases or if the logic changes.
parser.print_help()
parser.exit(1)
def decrypt_all(out_dir: str | Path | None = None) -> None:
"""Decrypt all accessories and save them to the specified directory as JSON files."""
def get_path(d, acc) -> Path | None: # noqa: ANN001
if out_dir is None:
return None
d = Path(d)
d = d.resolve().absolute()
d.mkdir(parents=True, exist_ok=True)
return d / f"{acc.identifier}.json"
accs = list_accessories()
jsons = [acc.to_json(get_path(out_dir, acc)) for acc in accs]
print(json.dumps(jsons, indent=4, ensure_ascii=False)) # noqa: T201
if __name__ == "__main__":
main()