bars-icloud-drive/icloudpy/cmdline.py

375 lines
12 KiB
Python
Raw Normal View History

2024-12-17 10:40:56 +00:00
#! /usr/bin/env python
"""
A Command Line Wrapper to allow easy use of iCloudPy for
command line scripts, and related.
"""
# from builtins import input
import argparse
import pickle
import sys
from click import confirm
from icloudpy import ICloudPyService, utils
from icloudpy.exceptions import ICloudPyFailedLoginException
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.
"""
pickle_file = open(filename, "wb")
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
pickle_file.close()
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.",
)
# Path to session directory
parser.add_argument(
"--session-directory",
action="store",
dest="session_directory",
default=None,
help="Path to save session information",
)
# Server region - global or china
parser.add_argument(
"--region",
action="store",
dest="region",
default="global",
help="Server region - global or china",
)
command_line = parser.parse_args(args)
username = command_line.username
password = command_line.password
session_directory = command_line.session_directory
server_region = command_line.region
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 = (
ICloudPyService(
apple_id=username.strip(),
password=password.strip(),
cookie_directory=session_directory,
home_endpoint="https://www.icloud.com.cn",
setup_endpoint="https://setup.icloud.com.cn/setup/ws/1",
)
if server_region == "china"
else ICloudPyService(
apple_id=username.strip(),
password=password.strip(),
cookie_directory=session_directory,
)
)
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(
f' {i}: {device.get("deviceName", "SMS to " + 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 ICloudPyFailedLoginException as error:
# 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 = f"Bad username or password for {username}"
password = None
failure_count += 1
if failure_count >= 3:
raise RuntimeError(message) from error
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(f"{key} - {contents[key]}")
elif command_line.list:
print("-" * 30)
print(f"Name - {contents['name']}")
print(f"Display Name - {contents['deviceDisplayName']}")
print(f"Location - {contents['location']}")
print(f"Battery Level - {contents['batteryLevel']}")
print(f"Battery Status- {contents['batteryStatus']}")
print(f"Device Class - {contents['deviceClass']}")
print(f"Device Model - {contents['deviceModel']}")
# Play a Sound on a device
if command_line.sound:
if command_line.device_id:
dev.play_sound()
else:
raise RuntimeError(
f"\n\n\t\tSounds can only be played on a singular device. {DEVICE_ERROR}\n\n"
)
# 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(
f"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(
f"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(
f"Lost Mode can only be activated on a singular device. {DEVICE_ERROR}"
)
sys.exit(0)
if __name__ == "__main__":
main()