Skip to content

Commit

Permalink
Merge branch 'main' into feat/better-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
malmeloo committed Sep 3, 2024
2 parents 74d5e10 + 6b3dfdd commit eb7ecd6
Show file tree
Hide file tree
Showing 37 changed files with 1,182 additions and 371 deletions.
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use nix
43 changes: 43 additions & 0 deletions .github/actions/setup-project/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Common Python + Poetry Setup

inputs:
dependency-groups:
description: 'A comma-separated list of dependency groups to install'
default: 'main'
python-version:
description: 'The Python version to use'
default: '3.10'

runs:
using: 'composite'

steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}

- name: Install poetry
shell: bash
run: |
python -m pip install poetry
poetry config virtualenvs.in-project true
- name: Get cache key
id: cache-key
shell: bash
run: |
key=$(echo "${{ inputs.dependency-groups }}" | sed 's/,/+/')
echo "key=$key" >> "$GITHUB_OUTPUT"
- name: Load cached venv
id: cache-dependencies
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ runner.os }}-python-${{ inputs.python-version }}-groups-${{ steps.cache-key.outputs.key }}-${{ hashFiles('**/poetry.lock') }}

- name: Install dependencies
if: steps.cache-dependencies.outputs.cache-hit != 'true'
shell: bash
run: poetry install --with ${{ inputs.dependency-groups }}
13 changes: 3 additions & 10 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,10 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5

- uses: './.github/actions/setup-project'
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install poetry
poetry config virtualenvs.in-project true
poetry install
dependency-groups: 'docs'

- name: Build documentation
run: |
Expand Down
17 changes: 4 additions & 13 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,17 @@ name: Pre-commit
on:
workflow_dispatch:
push:
branches-ignore:
- main

jobs:
deploy:
check:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install poetry
poetry config virtualenvs.in-project true
poetry install
- uses: './.github/actions/setup-project'
with:
dependency-groups: 'dev,test'

- uses: pre-commit/[email protected]

Expand Down
15 changes: 6 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5

- uses: './.github/actions/setup-project'
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install poetry
poetry config virtualenvs.in-project true
poetry install
dependency-groups: 'dev'

- name: Prepare README
run: ./scripts/refactor_readme.py README.md

- name: Build package
run: poetry build
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Run unit tests

on:
workflow_dispatch:
push:

jobs:
versions:
runs-on: ubuntu-latest

outputs:
py-versions: ${{ steps.supported-versions.outputs.py-versions }}

steps:
- uses: actions/checkout@v4

- uses: './.github/actions/setup-project'
with:
dependency-groups: 'dev'

- id: supported-versions
name: Get supported versions
run: |
set -e
echo "py-versions=$(poetry run ./scripts/supported_py_versions.py)" >> "$GITHUB_OUTPUT"
test:
runs-on: ubuntu-latest

needs: versions
strategy:
matrix:
py-version: ${{ fromJson(needs.versions.outputs.py-versions) }}

steps:
- uses: actions/checkout@v4

- uses: './.github/actions/setup-project'
with:
python-version: ${{ matrix.py-version }}
dependency-groups: 'test'

- name: Run unit tests
run: poetry run pytest

results:
runs-on: ubuntu-latest
needs: test
steps:
- run: |
result="${{ needs.test.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,4 @@ cython_debug/
account.json
airtag.plist
DO_NOT_COMMIT*
.direnv/
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.9
rev: v0.6.3
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.350
rev: v1.1.378
hooks:
- id: pyright
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# FindMy.py

[![](https://img.shields.io/pypi/v/FindMy)](https://pypi.org/project/FindMy/)
[![](https://img.shields.io/pypi/dm/FindMy)](#)
[![](https://img.shields.io/github/license/malmeloo/FindMy.py)](LICENSE.md)
[![](https://img.shields.io/pypi/pyversions/FindMy)](#)

Expand All @@ -19,13 +20,15 @@ application wishing to integrate with the Find My network.
> without prior warning.
>
> You are encouraged to report any issues you can find on the
> [issue tracker](https://github.com/malmeloo/FindMy.py/)!
> [issue tracker](https://github.com/malmeloo/FindMy.py/issues/)!
### Features

- [x] Cross-platform: no Mac needed
- [x] Fetch location reports
- [x] Apple acount sign-in
- [x] Fetch and decrypt location reports
- [x] Official accessories (AirTags, iDevices, etc.)
- [x] Custom AirTags (OpenHaystack)
- [x] Apple account sign-in
- [x] SMS 2FA support
- [x] Trusted Device 2FA support
- [x] Scan for nearby FindMy-devices
Expand All @@ -36,8 +39,7 @@ application wishing to integrate with the Find My network.
### Roadmap

- [ ] Local anisette generation (without server)
- Can be done using [pyprovision](https://github.com/Dadoum/pyprovision/),
however I want to wait until Python wheels are available.
- More information: [#2](https://github.com/malmeloo/FindMy.py/issues/2)

## Installation

Expand Down
2 changes: 2 additions & 0 deletions examples/_login.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# ruff: noqa: ASYNC230

import json
from pathlib import Path

Expand Down
60 changes: 50 additions & 10 deletions examples/device_scanner.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,67 @@
import asyncio
import logging

from findmy.scanner import OfflineFindingScanner
from findmy import KeyPair
from findmy.scanner import (
NearbyOfflineFindingDevice,
OfflineFindingScanner,
SeparatedOfflineFindingDevice,
)

logging.basicConfig(level=logging.INFO)

# Set if you want to check whether a specific key (or accessory!) is in the scan results.
# Make sure to enter its private key!
# Leave empty (= None) to not check.
CHECK_KEY = KeyPair.from_b64("")


def _print_nearby(device: NearbyOfflineFindingDevice) -> None:
print(f"NEARBY Device - {device.mac_address}")
print(f" Status byte: {device.status:x}")
print(" Extra data:")
for k, v in sorted(device.additional_data.items()):
print(f" {k:20}: {v}")
print()


def _print_separated(device: SeparatedOfflineFindingDevice) -> None:
print(f"SEPARATED Device - {device.mac_address}")
print(f" Public key: {device.adv_key_b64}")
print(f" Lookup key: {device.hashed_adv_key_b64}")
print(f" Status byte: {device.status:x}")
print(f" Hint byte: {device.hint:x}")
print(" Extra data:")
for k, v in sorted(device.additional_data.items()):
print(f" {k:20}: {v}")
print()


async def scan() -> None:
scanner = await OfflineFindingScanner.create()

print("Scanning for FindMy-devices...")
print()

scan_device = None

async for device in scanner.scan_for(10, extend_timeout=True):
print(f"Device - {device.mac_address}")
print(f" Public key: {device.adv_key_b64}")
print(f" Lookup key: {device.hashed_adv_key_b64}")
print(f" Status byte: {device.status:x}")
print(f" Hint byte: {device.hint:x}")
print(" Extra data:")
for k, v in sorted(device.additional_data.items()):
print(f" {k:20}: {v}")
print()
if isinstance(device, NearbyOfflineFindingDevice):
_print_nearby(device)
elif isinstance(device, SeparatedOfflineFindingDevice):
_print_separated(device)
else:
print(f"Unknown device: {device}")
print()
continue

if CHECK_KEY and device.is_from(CHECK_KEY):
scan_device = device

if scan_device:
print("Key or accessory was found in scan results! :D")
elif CHECK_KEY:
print("Selected key or accessory was not found in scan results... :c")


if __name__ == "__main__":
Expand Down
31 changes: 16 additions & 15 deletions examples/fetch_reports.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import sys

from _login import get_account_sync

Expand All @@ -8,30 +9,30 @@
# URL to (public or local) anisette server
ANISETTE_SERVER = "http://localhost:6969"

# Private base64-encoded key to look up
KEY_PRIV = ""
logging.basicConfig(level=logging.INFO)

# Optional, to verify that advertisement key derivation works for your key
KEY_ADV = ""

logging.basicConfig(level=logging.DEBUG)


def fetch_reports(lookup_key: KeyPair) -> None:
anisette = RemoteAnisetteProvider(ANISETTE_SERVER)
acc = get_account_sync(anisette)
def fetch_reports(priv_key: str) -> int:
key = KeyPair.from_b64(priv_key)
acc = get_account_sync(
RemoteAnisetteProvider(ANISETTE_SERVER),
)

print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})")

# It's that simple!
reports = acc.fetch_last_reports([lookup_key])[lookup_key]
reports = acc.fetch_last_reports(key)
for report in sorted(reports):
print(report)

return 1


if __name__ == "__main__":
key = KeyPair.from_b64(KEY_PRIV)
if KEY_ADV: # verify that your adv key is correct :D
assert key.adv_key_b64 == KEY_ADV
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <private key>", file=sys.stderr)
print(file=sys.stderr)
print("The private key should be base64-encoded.", file=sys.stderr)
sys.exit(1)

fetch_reports(key)
sys.exit(fetch_reports(sys.argv[1]))
Loading

0 comments on commit eb7ecd6

Please sign in to comment.