Coverage for icloudpy/cmdline.py: 63%
127 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-04-12 14:26 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-04-12 14:26 +0000
1#! /usr/bin/env python
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=(
53 "Apple ID Password to Use; if unspecified, password will be "
54 "fetched from the system keyring."
55 ),
56 )
57 parser.add_argument(
58 "-n",
59 "--non-interactive",
60 action="store_false",
61 dest="interactive",
62 default=True,
63 help="Disable interactive prompts.",
64 )
65 parser.add_argument(
66 "--delete-from-keyring",
67 action="store_true",
68 dest="delete_from_keyring",
69 default=False,
70 help="Delete stored password in system keyring for this username.",
71 )
72 parser.add_argument(
73 "--list",
74 action="store_true",
75 dest="list",
76 default=False,
77 help="Short Listings for Device(s) associated with account",
78 )
79 parser.add_argument(
80 "--llist",
81 action="store_true",
82 dest="longlist",
83 default=False,
84 help="Detailed Listings for Device(s) associated with account",
85 )
86 parser.add_argument(
87 "--locate",
88 action="store_true",
89 dest="locate",
90 default=False,
91 help="Retrieve Location for the iDevice (non-exclusive).",
92 )
94 # Restrict actions to a specific devices UID / DID
95 parser.add_argument(
96 "--device",
97 action="store",
98 dest="device_id",
99 default=False,
100 help="Only effect this device",
101 )
103 # Trigger Sound Alert
104 parser.add_argument(
105 "--sound",
106 action="store_true",
107 dest="sound",
108 default=False,
109 help="Play a sound on the device",
110 )
112 # Trigger Message w/Sound Alert
113 parser.add_argument(
114 "--message",
115 action="store",
116 dest="message",
117 default=False,
118 help="Optional Text Message to display with a sound",
119 )
121 # Trigger Message (without Sound) Alert
122 parser.add_argument(
123 "--silentmessage",
124 action="store",
125 dest="silentmessage",
126 default=False,
127 help="Optional Text Message to display with no sounds",
128 )
130 # Lost Mode
131 parser.add_argument(
132 "--lostmode",
133 action="store_true",
134 dest="lostmode",
135 default=False,
136 help="Enable Lost mode for the device",
137 )
138 parser.add_argument(
139 "--lostphone",
140 action="store",
141 dest="lost_phone",
142 default=False,
143 help="Phone Number allowed to call when lost mode is enabled",
144 )
145 parser.add_argument(
146 "--lostpassword",
147 action="store",
148 dest="lost_password",
149 default=False,
150 help="Forcibly active this passcode on the idevice",
151 )
152 parser.add_argument(
153 "--lostmessage",
154 action="store",
155 dest="lost_message",
156 default="",
157 help="Forcibly display this message when activating lost mode.",
158 )
160 # Output device data to an pickle file
161 parser.add_argument(
162 "--outputfile",
163 action="store_true",
164 dest="output_to_file",
165 default="",
166 help="Save device data to a file in the current directory.",
167 )
169 # Path to session directory
170 parser.add_argument(
171 "--session-directory",
172 action="store",
173 dest="session_directory",
174 default=None,
175 help="Path to save session information",
176 )
178 # Server region - global or china
179 parser.add_argument(
180 "--region",
181 action="store",
182 dest="region",
183 default="global",
184 help="Server region - global or china",
185 )
187 command_line = parser.parse_args(args)
189 username = command_line.username
190 password = command_line.password
191 session_directory = command_line.session_directory
192 server_region = command_line.region
194 if username and command_line.delete_from_keyring:
195 utils.delete_password_in_keyring(username)
197 failure_count = 0
198 while True:
199 # Which password we use is determined by your username, so we
200 # do need to check for this first and separately.
201 if not username:
202 parser.error("No username supplied")
204 if not password:
205 password = utils.get_password(
206 username, interactive=command_line.interactive
207 )
209 if not password:
210 parser.error("No password supplied")
212 try:
213 api = (
214 ICloudPyService(
215 apple_id=username.strip(),
216 password=password.strip(),
217 cookie_directory=session_directory,
218 home_endpoint="https://www.icloud.com.cn",
219 setup_endpoint="https://setup.icloud.com.cn/setup/ws/1",
220 )
221 if server_region == "china"
222 else ICloudPyService(
223 apple_id=username.strip(),
224 password=password.strip(),
225 cookie_directory=session_directory,
226 )
227 )
229 if (
230 not utils.password_exists_in_keyring(username)
231 and command_line.interactive
232 and confirm("Save password in keyring?")
233 ):
234 utils.store_password_in_keyring(username, password)
236 if api.requires_2fa:
237 # fmt: off
238 print(
239 "\nTwo-step authentication required.",
240 "\nPlease enter validation code"
241 )
242 # fmt: on
244 code = input("(string) --> ")
245 if not api.validate_2fa_code(code):
246 print("Failed to verify verification code")
247 sys.exit(1)
249 print("")
251 elif api.requires_2sa:
252 # fmt: off
253 print(
254 "\nTwo-step authentication required.",
255 "\nYour trusted devices are:"
256 )
257 # fmt: on
259 devices = api.trusted_devices
260 for i, device in enumerate(devices):
261 print(
262 f' {i}: {device.get("deviceName", "SMS to " + device.get("phoneNumber"))}'
263 )
265 print("\nWhich device would you like to use?")
266 device = int(input("(number) --> "))
267 device = devices[device]
268 if not api.send_verification_code(device):
269 print("Failed to send verification code")
270 sys.exit(1)
272 print("\nPlease enter validation code")
273 code = input("(string) --> ")
274 if not api.validate_verification_code(device, code):
275 print("Failed to verify verification code")
276 sys.exit(1)
278 print("")
279 break
280 except ICloudPyFailedLoginException as error:
281 # If they have a stored password; we just used it and
282 # it did not work; let's delete it if there is one.
283 if utils.password_exists_in_keyring(username):
284 utils.delete_password_in_keyring(username)
286 message = f"Bad username or password for {username}"
287 password = None
289 failure_count += 1
290 if failure_count >= 3:
291 raise RuntimeError(message) from error
293 print(message, file=sys.stderr)
295 for dev in api.devices:
296 if not command_line.device_id or (
297 command_line.device_id.strip().lower() == dev.content["id"].strip().lower()
298 ):
299 # List device(s)
300 if command_line.locate:
301 dev.location()
303 if command_line.output_to_file:
304 create_pickled_data(
305 dev,
306 filename=(dev.content["name"].strip().lower() + ".fmip_snapshot"),
307 )
309 contents = dev.content
310 if command_line.longlist:
311 print("-" * 30)
312 print(contents["name"])
313 for key in contents:
314 print(f"{key} - {contents[key]}")
315 elif command_line.list:
316 print("-" * 30)
317 print(f"Name - {contents['name']}")
318 print(f"Display Name - {contents['deviceDisplayName']}")
319 print(f"Location - {contents['location']}")
320 print(f"Battery Level - {contents['batteryLevel']}")
321 print(f"Battery Status- {contents['batteryStatus']}")
322 print(f"Device Class - {contents['deviceClass']}")
323 print(f"Device Model - {contents['deviceModel']}")
325 # Play a Sound on a device
326 if command_line.sound:
327 if command_line.device_id:
328 dev.play_sound()
329 else:
330 raise RuntimeError(
331 f"\n\n\t\tSounds can only be played on a singular device. {DEVICE_ERROR}\n\n"
332 )
334 # Display a Message on the device
335 if command_line.message:
336 if command_line.device_id:
337 dev.display_message(
338 subject="A Message", message=command_line.message, sounds=True
339 )
340 else:
341 raise RuntimeError(
342 f"Messages can only be played on a singular device. {DEVICE_ERROR}"
343 )
345 # Display a Silent Message on the device
346 if command_line.silentmessage:
347 if command_line.device_id:
348 dev.display_message(
349 subject="A Silent Message",
350 message=command_line.silentmessage,
351 sounds=False,
352 )
353 else:
354 raise RuntimeError(
355 f"Silent Messages can only be played on a singular device. {DEVICE_ERROR}"
356 )
358 # Enable Lost mode
359 if command_line.lostmode:
360 if command_line.device_id:
361 dev.lost_device(
362 number=command_line.lost_phone.strip(),
363 text=command_line.lost_message.strip(),
364 newpasscode=command_line.lost_password.strip(),
365 )
366 else:
367 raise RuntimeError(
368 f"Lost Mode can only be activated on a singular device. {DEVICE_ERROR}"
369 )
370 sys.exit(0)
373if __name__ == "__main__":
374 main()