Coverage for icloudpy/cmdline.py: 63%
126 statements
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-30 19:31 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2024-12-30 19:31 +0000
1#! /usr/bin/env python # noqa:EXE001
2"""
3A Command Line Wrapper to allow easy use of iCloudPy for
4command line scripts, and related.
5"""
7# from builtins import input
8import argparse
9import pickle
10import sys
12from click import confirm
14from icloudpy import ICloudPyService, utils
15from icloudpy.exceptions import ICloudPyFailedLoginException
17DEVICE_ERROR = "Please use the --device switch to indicate which device to use."
20def create_pickled_data(idevice, filename):
21 """
22 This helper will output the idevice to a pickled file named
23 after the passed filename.
25 This allows the data to be used without resorting to screen / pipe
26 scrapping.
27 """
28 pickle_file = open(filename, "wb")
29 pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
30 pickle_file.close()
33def main(args=None):
34 """Main commandline entrypoint."""
35 if args is None:
36 args = sys.argv[1:]
38 parser = argparse.ArgumentParser(description="Find My iPhone CommandLine Tool")
40 parser.add_argument(
41 "--username",
42 action="store",
43 dest="username",
44 default="",
45 help="Apple ID to Use",
46 )
47 parser.add_argument(
48 "--password",
49 action="store",
50 dest="password",
51 default="",
52 help=("Apple ID Password to Use; if unspecified, password will be " "fetched from the system keyring."),
53 )
54 parser.add_argument(
55 "-n",
56 "--non-interactive",
57 action="store_false",
58 dest="interactive",
59 default=True,
60 help="Disable interactive prompts.",
61 )
62 parser.add_argument(
63 "--delete-from-keyring",
64 action="store_true",
65 dest="delete_from_keyring",
66 default=False,
67 help="Delete stored password in system keyring for this username.",
68 )
69 parser.add_argument(
70 "--list",
71 action="store_true",
72 dest="list",
73 default=False,
74 help="Short Listings for Device(s) associated with account",
75 )
76 parser.add_argument(
77 "--llist",
78 action="store_true",
79 dest="longlist",
80 default=False,
81 help="Detailed Listings for Device(s) associated with account",
82 )
83 parser.add_argument(
84 "--locate",
85 action="store_true",
86 dest="locate",
87 default=False,
88 help="Retrieve Location for the iDevice (non-exclusive).",
89 )
91 # Restrict actions to a specific devices UID / DID
92 parser.add_argument(
93 "--device",
94 action="store",
95 dest="device_id",
96 default=False,
97 help="Only effect this device",
98 )
100 # Trigger Sound Alert
101 parser.add_argument(
102 "--sound",
103 action="store_true",
104 dest="sound",
105 default=False,
106 help="Play a sound on the device",
107 )
109 # Trigger Message w/Sound Alert
110 parser.add_argument(
111 "--message",
112 action="store",
113 dest="message",
114 default=False,
115 help="Optional Text Message to display with a sound",
116 )
118 # Trigger Message (without Sound) Alert
119 parser.add_argument(
120 "--silentmessage",
121 action="store",
122 dest="silentmessage",
123 default=False,
124 help="Optional Text Message to display with no sounds",
125 )
127 # Lost Mode
128 parser.add_argument(
129 "--lostmode",
130 action="store_true",
131 dest="lostmode",
132 default=False,
133 help="Enable Lost mode for the device",
134 )
135 parser.add_argument(
136 "--lostphone",
137 action="store",
138 dest="lost_phone",
139 default=False,
140 help="Phone Number allowed to call when lost mode is enabled",
141 )
142 parser.add_argument(
143 "--lostpassword",
144 action="store",
145 dest="lost_password",
146 default=False,
147 help="Forcibly active this passcode on the idevice",
148 )
149 parser.add_argument(
150 "--lostmessage",
151 action="store",
152 dest="lost_message",
153 default="",
154 help="Forcibly display this message when activating lost mode.",
155 )
157 # Output device data to an pickle file
158 parser.add_argument(
159 "--outputfile",
160 action="store_true",
161 dest="output_to_file",
162 default="",
163 help="Save device data to a file in the current directory.",
164 )
166 # Path to session directory
167 parser.add_argument(
168 "--session-directory",
169 action="store",
170 dest="session_directory",
171 default=None,
172 help="Path to save session information",
173 )
175 # Server region - global or china
176 parser.add_argument(
177 "--region",
178 action="store",
179 dest="region",
180 default="global",
181 help="Server region - global or china",
182 )
184 command_line = parser.parse_args(args)
186 username = command_line.username
187 password = command_line.password
188 session_directory = command_line.session_directory
189 server_region = command_line.region
191 if username and command_line.delete_from_keyring:
192 utils.delete_password_in_keyring(username)
194 failure_count = 0
195 while True:
196 # Which password we use is determined by your username, so we
197 # do need to check for this first and separately.
198 if not username:
199 parser.error("No username supplied")
201 if not password:
202 password = utils.get_password(
203 username,
204 interactive=command_line.interactive,
205 )
207 if not password:
208 parser.error("No password supplied")
210 try:
211 api = (
212 ICloudPyService(
213 apple_id=username.strip(),
214 password=password.strip(),
215 cookie_directory=session_directory,
216 home_endpoint="https://www.icloud.com.cn",
217 setup_endpoint="https://setup.icloud.com.cn/setup/ws/1",
218 )
219 if server_region == "china"
220 else ICloudPyService(
221 apple_id=username.strip(),
222 password=password.strip(),
223 cookie_directory=session_directory,
224 )
225 )
227 if (
228 not utils.password_exists_in_keyring(username)
229 and command_line.interactive
230 and confirm("Save password in keyring?")
231 ):
232 utils.store_password_in_keyring(username, password)
234 if api.requires_2fa:
235 # fmt: off
236 print(
237 "\nTwo-step authentication required.",
238 "\nPlease enter validation code",
239 )
240 # fmt: on
242 code = input("(string) --> ")
243 if not api.validate_2fa_code(code):
244 print("Failed to verify verification code")
245 sys.exit(1)
247 print("")
249 elif api.requires_2sa:
250 # fmt: off
251 print(
252 "\nTwo-step authentication required.",
253 "\nYour trusted devices are:",
254 )
255 # fmt: on
257 devices = api.trusted_devices
258 for i, device in enumerate(devices):
259 print(
260 f' {i}: {device.get("deviceName", "SMS to " + device.get("phoneNumber"))}',
261 )
263 print("\nWhich device would you like to use?")
264 device = int(input("(number) --> "))
265 device = devices[device]
266 if not api.send_verification_code(device):
267 print("Failed to send verification code")
268 sys.exit(1)
270 print("\nPlease enter validation code")
271 code = input("(string) --> ")
272 if not api.validate_verification_code(device, code):
273 print("Failed to verify verification code")
274 sys.exit(1)
276 print("")
277 break
278 except ICloudPyFailedLoginException as error:
279 # If they have a stored password; we just used it and
280 # it did not work; let's delete it if there is one.
281 if utils.password_exists_in_keyring(username):
282 utils.delete_password_in_keyring(username)
284 message = f"Bad username or password for {username}"
285 password = None
287 failure_count += 1
288 if failure_count >= 3:
289 raise RuntimeError(message) from error
291 print(message, file=sys.stderr)
293 for dev in api.devices:
294 if not command_line.device_id or (command_line.device_id.strip().lower() == dev.content["id"].strip().lower()):
295 # List device(s)
296 if command_line.locate:
297 dev.location()
299 if command_line.output_to_file:
300 create_pickled_data(
301 dev,
302 filename=(dev.content["name"].strip().lower() + ".fmip_snapshot"),
303 )
305 contents = dev.content
306 if command_line.longlist:
307 print("-" * 30)
308 print(contents["name"])
309 for key in contents:
310 print(f"{key} - {contents[key]}")
311 elif command_line.list:
312 print("-" * 30)
313 print(f"Name - {contents['name']}")
314 print(f"Display Name - {contents['deviceDisplayName']}")
315 print(f"Location - {contents['location']}")
316 print(f"Battery Level - {contents['batteryLevel']}")
317 print(f"Battery Status- {contents['batteryStatus']}")
318 print(f"Device Class - {contents['deviceClass']}")
319 print(f"Device Model - {contents['deviceModel']}")
321 # Play a Sound on a device
322 if command_line.sound:
323 if command_line.device_id:
324 dev.play_sound()
325 else:
326 raise RuntimeError(
327 f"\n\n\t\tSounds can only be played on a singular device. {DEVICE_ERROR}\n\n",
328 )
330 # Display a Message on the device
331 if command_line.message:
332 if command_line.device_id:
333 dev.display_message(
334 subject="A Message",
335 message=command_line.message,
336 sounds=True,
337 )
338 else:
339 raise RuntimeError(
340 f"Messages can only be played on a singular device. {DEVICE_ERROR}",
341 )
343 # Display a Silent Message on the device
344 if command_line.silentmessage:
345 if command_line.device_id:
346 dev.display_message(
347 subject="A Silent Message",
348 message=command_line.silentmessage,
349 sounds=False,
350 )
351 else:
352 raise RuntimeError(
353 f"Silent Messages can only be played on a singular device. {DEVICE_ERROR}",
354 )
356 # Enable Lost mode
357 if command_line.lostmode:
358 if command_line.device_id:
359 dev.lost_device(
360 number=command_line.lost_phone.strip(),
361 text=command_line.lost_message.strip(),
362 newpasscode=command_line.lost_password.strip(),
363 )
364 else:
365 raise RuntimeError(
366 f"Lost Mode can only be activated on a singular device. {DEVICE_ERROR}",
367 )
368 sys.exit(0)
371if __name__ == "__main__":
372 main()