pyicloud/pyicloud/cmdline.py
Hugo 592ff464c5
Support Python 3.6 to 3.10 (#371)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Quentin POLLET <polletquentin74@me.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-16 11:00:29 -08:00

362 lines
11 KiB
Python

#! /usr/bin/env python
"""
A Command Line Wrapper to allow easy use of pyicloud for
command line scripts, and related.
"""
import argparse
import pickle
import sys
from click import confirm
from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException
from . import utils
DEVICE_ERROR = "Please use the --device switch to indicate which device to use."
def create_pickled_data(idevice, filename):
"""
This helper will output the idevice to a pickled file named
after the passed filename.
This allows the data to be used without resorting to screen / pipe
scrapping.
"""
with open(filename, "wb") as pickle_file:
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
def main(args=None):
"""Main commandline entrypoint."""
if args is None:
args = sys.argv[1:]
parser = argparse.ArgumentParser(description="Find My iPhone CommandLine Tool")
parser.add_argument(
"--username",
action="store",
dest="username",
default="",
help="Apple ID to Use",
)
parser.add_argument(
"--password",
action="store",
dest="password",
default="",
help=(
"Apple ID Password to Use; if unspecified, password will be "
"fetched from the system keyring."
),
)
parser.add_argument(
"-n",
"--non-interactive",
action="store_false",
dest="interactive",
default=True,
help="Disable interactive prompts.",
)
parser.add_argument(
"--delete-from-keyring",
action="store_true",
dest="delete_from_keyring",
default=False,
help="Delete stored password in system keyring for this username.",
)
parser.add_argument(
"--list",
action="store_true",
dest="list",
default=False,
help="Short Listings for Device(s) associated with account",
)
parser.add_argument(
"--llist",
action="store_true",
dest="longlist",
default=False,
help="Detailed Listings for Device(s) associated with account",
)
parser.add_argument(
"--locate",
action="store_true",
dest="locate",
default=False,
help="Retrieve Location for the iDevice (non-exclusive).",
)
# Restrict actions to a specific devices UID / DID
parser.add_argument(
"--device",
action="store",
dest="device_id",
default=False,
help="Only effect this device",
)
# Trigger Sound Alert
parser.add_argument(
"--sound",
action="store_true",
dest="sound",
default=False,
help="Play a sound on the device",
)
# Trigger Message w/Sound Alert
parser.add_argument(
"--message",
action="store",
dest="message",
default=False,
help="Optional Text Message to display with a sound",
)
# Trigger Message (without Sound) Alert
parser.add_argument(
"--silentmessage",
action="store",
dest="silentmessage",
default=False,
help="Optional Text Message to display with no sounds",
)
# Lost Mode
parser.add_argument(
"--lostmode",
action="store_true",
dest="lostmode",
default=False,
help="Enable Lost mode for the device",
)
parser.add_argument(
"--lostphone",
action="store",
dest="lost_phone",
default=False,
help="Phone Number allowed to call when lost mode is enabled",
)
parser.add_argument(
"--lostpassword",
action="store",
dest="lost_password",
default=False,
help="Forcibly active this passcode on the idevice",
)
parser.add_argument(
"--lostmessage",
action="store",
dest="lost_message",
default="",
help="Forcibly display this message when activating lost mode.",
)
# Output device data to an pickle file
parser.add_argument(
"--outputfile",
action="store_true",
dest="output_to_file",
default="",
help="Save device data to a file in the current directory.",
)
command_line = parser.parse_args(args)
username = command_line.username
password = command_line.password
if username and command_line.delete_from_keyring:
utils.delete_password_in_keyring(username)
failure_count = 0
while True:
# Which password we use is determined by your username, so we
# do need to check for this first and separately.
if not username:
parser.error("No username supplied")
if not password:
password = utils.get_password(
username, interactive=command_line.interactive
)
if not password:
parser.error("No password supplied")
try:
api = PyiCloudService(username.strip(), password.strip())
if (
not utils.password_exists_in_keyring(username)
and command_line.interactive
and confirm("Save password in keyring?")
):
utils.store_password_in_keyring(username, password)
if api.requires_2fa:
# fmt: off
print(
"\nTwo-step authentication required.",
"\nPlease enter validation code"
)
# fmt: on
code = input("(string) --> ")
if not api.validate_2fa_code(code):
print("Failed to verify verification code")
sys.exit(1)
print("")
elif api.requires_2sa:
# fmt: off
print(
"\nTwo-step authentication required.",
"\nYour trusted devices are:"
)
# fmt: on
devices = api.trusted_devices
for i, device in enumerate(devices):
print(
" %s: %s"
% (
i,
device.get(
"deviceName", "SMS to %s" % device.get("phoneNumber")
),
)
)
print("\nWhich device would you like to use?")
device = int(input("(number) --> "))
device = devices[device]
if not api.send_verification_code(device):
print("Failed to send verification code")
sys.exit(1)
print("\nPlease enter validation code")
code = input("(string) --> ")
if not api.validate_verification_code(device, code):
print("Failed to verify verification code")
sys.exit(1)
print("")
break
except PyiCloudFailedLoginException as err:
# If they have a stored password; we just used it and
# it did not work; let's delete it if there is one.
if utils.password_exists_in_keyring(username):
utils.delete_password_in_keyring(username)
message = "Bad username or password for {username}".format(
username=username,
)
password = None
failure_count += 1
if failure_count >= 3:
raise RuntimeError(message) from err
print(message, file=sys.stderr)
for dev in api.devices:
if not command_line.device_id or (
command_line.device_id.strip().lower() == dev.content["id"].strip().lower()
):
# List device(s)
if command_line.locate:
dev.location()
if command_line.output_to_file:
create_pickled_data(
dev,
filename=(dev.content["name"].strip().lower() + ".fmip_snapshot"),
)
contents = dev.content
if command_line.longlist:
print("-" * 30)
print(contents["name"])
for key in contents:
print("%20s - %s" % (key, contents[key]))
elif command_line.list:
print("-" * 30)
print("Name - %s" % contents["name"])
print("Display Name - %s" % contents["deviceDisplayName"])
print("Location - %s" % contents["location"])
print("Battery Level - %s" % contents["batteryLevel"])
print("Battery Status- %s" % contents["batteryStatus"])
print("Device Class - %s" % contents["deviceClass"])
print("Device Model - %s" % contents["deviceModel"])
# Play a Sound on a device
if command_line.sound:
if command_line.device_id:
dev.play_sound()
else:
raise RuntimeError(
"\n\n\t\t%s %s\n\n"
% (
"Sounds can only be played on a singular device.",
DEVICE_ERROR,
)
)
# Display a Message on the device
if command_line.message:
if command_line.device_id:
dev.display_message(
subject="A Message", message=command_line.message, sounds=True
)
else:
raise RuntimeError(
"%s %s"
% (
"Messages can only be played on a singular device.",
DEVICE_ERROR,
)
)
# Display a Silent Message on the device
if command_line.silentmessage:
if command_line.device_id:
dev.display_message(
subject="A Silent Message",
message=command_line.silentmessage,
sounds=False,
)
else:
raise RuntimeError(
"%s %s"
% (
"Silent Messages can only be played "
"on a singular device.",
DEVICE_ERROR,
)
)
# Enable Lost mode
if command_line.lostmode:
if command_line.device_id:
dev.lost_device(
number=command_line.lost_phone.strip(),
text=command_line.lost_message.strip(),
newpasscode=command_line.lost_password.strip(),
)
else:
raise RuntimeError(
"%s %s"
% (
"Lost Mode can only be activated on a singular device.",
DEVICE_ERROR,
)
)
sys.exit(0)
if __name__ == "__main__":
main()