44
55import argparse
66import configparser
7+ import contextlib
78import logging
89import os
910import stat
@@ -311,36 +312,60 @@ def fetch_zuliprc(zuliprc_path: str) -> None:
311312 print (in_color ("red" , "\n Incorrect Email(or Username) or Password!\n " ))
312313 login_data = get_api_key (realm_url )
313314
315+ zulip_key_path = os .path .join (
316+ os .path .dirname (os .path .abspath (zuliprc_path )), "zulip_key"
317+ )
318+
314319 preferred_realm_url , login_id , api_key = login_data
315320 save_zuliprc_failure = _write_zuliprc (
316- zuliprc_path ,
317- login_id = login_id ,
321+ to_path = zuliprc_path ,
322+ key_path = zulip_key_path ,
318323 api_key = api_key ,
324+ login_id = login_id ,
319325 server_url = preferred_realm_url ,
320326 )
321327 if not save_zuliprc_failure :
322- print (f"Generated API key saved at { zuliprc_path } " )
328+ print (f"Generated config file saved at { zuliprc_path } " )
323329 else :
324330 exit_with_error (save_zuliprc_failure )
325331
326332
327333def _write_zuliprc (
328- to_path : str , * , login_id : str , api_key : str , server_url : str
334+ to_path : str , * , key_path : str , login_id : str , api_key : str , server_url : str
329335) -> str :
330336 """
331- Writes a zuliprc file, returning a non-empty error string on failure
332- Only creates new private files; errors if file already exists
337+ Writes both zuliprc and zulip_key files securely.
338+ Ensures atomicity: if one file fails to write, cleans up the other.
339+ Returns an empty string on success, or a descriptive error message on failure.
333340 """
341+ zuliprc_created = False
342+
334343 try :
344+ # Write zuliprc
335345 with open (
336346 os .open (to_path , os .O_CREAT | os .O_WRONLY | os .O_EXCL , 0o600 ), "w"
337347 ) as f :
338- f .write (f"[api]\n email={ login_id } \n key={ api_key } \n site={ server_url } " )
348+ f .write (
349+ f"[api]\n email={ login_id } \n passcmd=cat zulip_key\n site={ server_url } "
350+ )
351+ zuliprc_created = True
352+ # Write zulip_key
353+ with open (
354+ os .open (key_path , os .O_CREAT | os .O_WRONLY | os .O_EXCL , 0o600 ), "w"
355+ ) as f :
356+ f .write (api_key )
357+
339358 return ""
359+
340360 except FileExistsError :
341- return f"zuliprc already exists at { to_path } "
361+ filename = to_path if not zuliprc_created else key_path
362+ return f"FileExistsError: { filename } already exists"
342363 except OSError as ex :
343- return f"{ ex .__class__ .__name__ } : zuliprc could not be created at { to_path } "
364+ if zuliprc_created :
365+ with contextlib .suppress (Exception ):
366+ os .remove (to_path )
367+ filename = key_path if zuliprc_created else to_path
368+ return f"{ ex .__class__ .__name__ } : could not create { filename } ({ ex } )"
344369
345370
346371def parse_zuliprc (zuliprc_str : str ) -> Dict [str , SettingData ]:
@@ -366,12 +391,12 @@ def parse_zuliprc(zuliprc_str: str) -> Dict[str, SettingData]:
366391 in_color (
367392 "red" ,
368393 "ERROR: Please ensure your zuliprc is NOT publicly accessible:\n "
369- " {0 }\n "
370- "(it currently has permissions '{1 }')\n "
394+ f " { zuliprc_path } \n "
395+ f "(it currently has permissions '{ stat . filemode ( mode ) } ')\n "
371396 "This can often be achieved with a command such as:\n "
372- " chmod og-rwx {0 }\n "
397+ f " chmod og-rwx { zuliprc_path } \n "
373398 "Consider regenerating the [api] part of your zuliprc to ensure "
374- "your account is secure." . format ( zuliprc_path , stat . filemode ( mode )) ,
399+ "your account is secure." ,
375400 )
376401 )
377402 sys .exit (1 )
@@ -687,8 +712,8 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
687712 # Dump stats only after temporary file is closed (for Win NT+ case)
688713 prof .dump_stats (profile_path )
689714 print (
690- "Profile data saved to {0 }.\n "
691- "You can visualize it using e.g. `snakeviz {0 }`" . format ( profile_path )
715+ f "Profile data saved to { profile_path } .\n "
716+ f "You can visualize it using e.g. `snakeviz { profile_path } `"
692717 )
693718
694719 sys .exit (1 )
0 commit comments