Coverage for icloudpy/cmdline.py: 63%

127 statements  

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

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

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 ) 

93 

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 ) 

102 

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 ) 

111 

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 ) 

120 

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 ) 

129 

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 ) 

159 

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 ) 

168 

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 ) 

177 

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 ) 

186 

187 command_line = parser.parse_args(args) 

188 

189 username = command_line.username 

190 password = command_line.password 

191 session_directory = command_line.session_directory 

192 server_region = command_line.region 

193 

194 if username and command_line.delete_from_keyring: 

195 utils.delete_password_in_keyring(username) 

196 

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

203 

204 if not password: 

205 password = utils.get_password( 

206 username, interactive=command_line.interactive 

207 ) 

208 

209 if not password: 

210 parser.error("No password supplied") 

211 

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 ) 

228 

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) 

235 

236 if api.requires_2fa: 

237 # fmt: off 

238 print( 

239 "\nTwo-step authentication required.", 

240 "\nPlease enter validation code" 

241 ) 

242 # fmt: on 

243 

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

245 if not api.validate_2fa_code(code): 

246 print("Failed to verify verification code") 

247 sys.exit(1) 

248 

249 print("") 

250 

251 elif api.requires_2sa: 

252 # fmt: off 

253 print( 

254 "\nTwo-step authentication required.", 

255 "\nYour trusted devices are:" 

256 ) 

257 # fmt: on 

258 

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 ) 

264 

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) 

271 

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) 

277 

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) 

285 

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

287 password = None 

288 

289 failure_count += 1 

290 if failure_count >= 3: 

291 raise RuntimeError(message) from error 

292 

293 print(message, file=sys.stderr) 

294 

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

302 

303 if command_line.output_to_file: 

304 create_pickled_data( 

305 dev, 

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

307 ) 

308 

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

324 

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 ) 

333 

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 ) 

344 

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 ) 

357 

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) 

371 

372 

373if __name__ == "__main__": 

374 main()