Coverage for icloudpy/cmdline.py: 63%

126 statements  

« 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""" 

6 

7# from builtins import input 

8import argparse 

9import pickle 

10import sys 

11 

12from click import confirm 

13 

14from icloudpy import ICloudPyService, utils 

15from icloudpy.exceptions import ICloudPyFailedLoginException 

16 

17DEVICE_ERROR = "Please use the --device switch to indicate which device to use." 

18 

19 

20def create_pickled_data(idevice, filename): 

21 """ 

22 This helper will output the idevice to a pickled file named 

23 after the passed filename. 

24 

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() 

31 

32 

33def main(args=None): 

34 """Main commandline entrypoint.""" 

35 if args is None: 

36 args = sys.argv[1:] 

37 

38 parser = argparse.ArgumentParser(description="Find My iPhone CommandLine Tool") 

39 

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 ) 

90 

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 ) 

99 

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 ) 

108 

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 ) 

117 

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 ) 

126 

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 ) 

156 

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 ) 

165 

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 ) 

174 

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 ) 

183 

184 command_line = parser.parse_args(args) 

185 

186 username = command_line.username 

187 password = command_line.password 

188 session_directory = command_line.session_directory 

189 server_region = command_line.region 

190 

191 if username and command_line.delete_from_keyring: 

192 utils.delete_password_in_keyring(username) 

193 

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") 

200 

201 if not password: 

202 password = utils.get_password( 

203 username, 

204 interactive=command_line.interactive, 

205 ) 

206 

207 if not password: 

208 parser.error("No password supplied") 

209 

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 ) 

226 

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) 

233 

234 if api.requires_2fa: 

235 # fmt: off 

236 print( 

237 "\nTwo-step authentication required.", 

238 "\nPlease enter validation code", 

239 ) 

240 # fmt: on 

241 

242 code = input("(string) --> ") 

243 if not api.validate_2fa_code(code): 

244 print("Failed to verify verification code") 

245 sys.exit(1) 

246 

247 print("") 

248 

249 elif api.requires_2sa: 

250 # fmt: off 

251 print( 

252 "\nTwo-step authentication required.", 

253 "\nYour trusted devices are:", 

254 ) 

255 # fmt: on 

256 

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 ) 

262 

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) 

269 

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) 

275 

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) 

283 

284 message = f"Bad username or password for {username}" 

285 password = None 

286 

287 failure_count += 1 

288 if failure_count >= 3: 

289 raise RuntimeError(message) from error 

290 

291 print(message, file=sys.stderr) 

292 

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() 

298 

299 if command_line.output_to_file: 

300 create_pickled_data( 

301 dev, 

302 filename=(dev.content["name"].strip().lower() + ".fmip_snapshot"), 

303 ) 

304 

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']}") 

320 

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 ) 

329 

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 ) 

342 

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 ) 

355 

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) 

369 

370 

371if __name__ == "__main__": 

372 main()