13
13
import tempfile
14
14
import textwrap
15
15
import time
16
+ from queue import Queue
17
+ from threading import Thread
16
18
from types import FrameType
17
19
from typing import IO , Any , Dict , List , NoReturn , Optional , Set , Tuple , Union
18
20
19
21
from typing_extensions import Literal , TypedDict
20
22
23
+ import zulip
21
24
from zulip import RandomExponentialBackoff
22
25
23
26
DEFAULT_SITE = "https://api.zulip.com"
24
27
25
28
26
29
class States :
27
- Startup , ZulipToZephyr , ZephyrToZulip , ChildSending = list (range (4 ))
30
+ Startup , ZulipToZephyr , ZephyrToZulip = list (range (3 ))
28
31
29
32
30
33
CURRENT_STATE = States .Startup
31
34
32
35
logger : logging .Logger
33
36
34
37
38
+ def make_zulip_client () -> zulip .Client :
39
+ return zulip .Client (
40
+ email = zulip_account_email ,
41
+ api_key = api_key ,
42
+ verbose = True ,
43
+ client = "zephyr_mirror" ,
44
+ site = options .site ,
45
+ )
46
+
47
+
35
48
def to_zulip_username (zephyr_username : str ) -> str :
36
49
if "@" in zephyr_username :
37
50
(user , realm ) = zephyr_username .split ("@" )
@@ -117,7 +130,7 @@ class ZephyrDict(TypedDict, total=False):
117
130
zsig : str
118
131
119
132
120
- def send_zulip (zeph : ZephyrDict ) -> Dict [str , Any ]:
133
+ def send_zulip (zulip_client : zulip . Client , zeph : ZephyrDict ) -> Dict [str , Any ]:
121
134
message : Dict [str , Any ]
122
135
message = {}
123
136
if options .forward_class_messages :
@@ -151,7 +164,7 @@ def send_zulip(zeph: ZephyrDict) -> Dict[str, Any]:
151
164
return zulip_client .send_message (message )
152
165
153
166
154
- def send_error_zulip (error_msg : str ) -> None :
167
+ def send_error_zulip (zulip_client : zulip . Client , error_msg : str ) -> None :
155
168
message = {
156
169
"type" : "private" ,
157
170
"sender" : zulip_account_email ,
@@ -263,7 +276,7 @@ def maybe_restart_mirroring_script() -> None:
263
276
raise Exception ("Failed to reload too many times, aborting!" )
264
277
265
278
266
- def process_loop (log : Optional [IO [str ]]) -> NoReturn :
279
+ def process_loop (zulip_queue : "Queue[ZephyrDict]" , log : Optional [IO [str ]]) -> NoReturn :
267
280
restart_check_count = 0
268
281
last_check_time = time .time ()
269
282
recieve_backoff = RandomExponentialBackoff ()
@@ -278,7 +291,7 @@ def process_loop(log: Optional[IO[str]]) -> NoReturn:
278
291
if notice is None :
279
292
break
280
293
try :
281
- process_notice (notice , log )
294
+ process_notice (notice , zulip_queue , log )
282
295
process_backoff .succeed ()
283
296
except Exception :
284
297
logger .exception ("Error relaying zephyr:" )
@@ -395,7 +408,9 @@ def decrypt_zephyr(zephyr_class: str, instance: str, body: str) -> str:
395
408
return decrypted
396
409
397
410
398
- def process_notice (notice : "zephyr.ZNotice" , log : Optional [IO [str ]]) -> None :
411
+ def process_notice (
412
+ notice : "zephyr.ZNotice" , zulip_queue : "Queue[ZephyrDict]" , log : Optional [IO [str ]]
413
+ ) -> None :
399
414
assert notice .sender is not None
400
415
(zsig , body ) = parse_zephyr_body (notice .message , notice .format )
401
416
is_personal = False
@@ -490,18 +505,19 @@ def process_notice(notice: "zephyr.ZNotice", log: Optional[IO[str]]) -> None:
490
505
log .write (json .dumps (zeph ) + "\n " )
491
506
log .flush ()
492
507
493
- if os .fork () == 0 :
494
- global CURRENT_STATE
495
- CURRENT_STATE = States .ChildSending
496
- # Actually send the message in a child process, to avoid blocking.
508
+ zulip_queue .put (zeph )
509
+
510
+
511
+ def send_zulip_worker (zulip_queue : "Queue[ZephyrDict]" , zulip_client : zulip .Client ) -> None :
512
+ while True :
513
+ zeph = zulip_queue .get ()
497
514
try :
498
- res = send_zulip (zeph )
515
+ res = send_zulip (zulip_client , zeph )
499
516
if res .get ("result" ) != "success" :
500
517
logger .error (f"Error relaying zephyr:\n { zeph } \n { res } " )
501
518
except Exception :
502
519
logger .exception ("Error relaying zephyr:" )
503
- finally :
504
- os ._exit (0 )
520
+ zulip_queue .task_done ()
505
521
506
522
507
523
def quit_failed_initialization (message : str ) -> str :
@@ -560,6 +576,8 @@ def zephyr_subscribe_autoretry(sub: Tuple[str, str, str]) -> None:
560
576
561
577
562
578
def zephyr_to_zulip (options : optparse .Values ) -> None :
579
+ zulip_client = make_zulip_client ()
580
+
563
581
if options .use_sessions and os .path .exists (options .session_path ):
564
582
logger .info ("Loading old session" )
565
583
zephyr_load_session_autoretry (options .session_path )
@@ -593,18 +611,22 @@ def zephyr_to_zulip(options: optparse.Values) -> None:
593
611
"sending saved message to %s from %s..."
594
612
% (zeph .get ("stream" , zeph .get ("recipient" )), zeph ["sender" ])
595
613
)
596
- send_zulip (zeph )
614
+ send_zulip (zulip_client , zeph )
597
615
except Exception :
598
616
logger .exception ("Could not send saved zephyr:" )
599
617
time .sleep (2 )
600
618
601
619
logger .info ("Successfully initialized; Starting receive loop." )
602
620
621
+ # Actually send the messages in a thread, to avoid blocking.
622
+ zulip_queue : "Queue[ZephyrDict]" = Queue ()
623
+ Thread (target = lambda : send_zulip_worker (zulip_queue , zulip_client )).start ()
624
+
603
625
if options .resend_log_path is not None :
604
626
with open (options .resend_log_path , "a" ) as log :
605
- process_loop (log )
627
+ process_loop (zulip_queue , log )
606
628
else :
607
- process_loop (None )
629
+ process_loop (zulip_queue , None )
608
630
609
631
610
632
def send_zephyr (zwrite_args : List [str ], content : str ) -> Tuple [int , str ]:
@@ -675,7 +697,7 @@ def zcrypt_encrypt_content(zephyr_class: str, instance: str, content: str) -> Op
675
697
return encrypted
676
698
677
699
678
- def forward_to_zephyr (message : Dict [str , Any ]) -> None :
700
+ def forward_to_zephyr (message : Dict [str , Any ], zulip_client : zulip . Client ) -> None :
679
701
# 'Any' can be of any type of text
680
702
support_heading = "Hi there! This is an automated message from Zulip."
681
703
support_closing = """If you have any questions, please be in touch through the \
@@ -749,6 +771,7 @@ def forward_to_zephyr(message: Dict[str, Any]) -> None:
749
771
result = zcrypt_encrypt_content (zephyr_class , instance , wrapped_content )
750
772
if result is None :
751
773
send_error_zulip (
774
+ zulip_client ,
752
775
"""%s
753
776
754
777
Your Zulip-Zephyr mirror bot was unable to forward that last message \
@@ -758,7 +781,7 @@ class and your mirroring bot does not have access to the relevant \
758
781
Zulip users (like you) received it, Zephyr users did not.
759
782
760
783
%s"""
761
- % (support_heading , support_closing )
784
+ % (support_heading , support_closing ),
762
785
)
763
786
return
764
787
@@ -775,6 +798,7 @@ class and your mirroring bot does not have access to the relevant \
775
798
return
776
799
elif code == 0 :
777
800
send_error_zulip (
801
+ zulip_client ,
778
802
"""%s
779
803
780
804
Your last message was successfully mirrored to zephyr, but zwrite \
@@ -783,7 +807,7 @@ class and your mirroring bot does not have access to the relevant \
783
807
%s
784
808
785
809
%s"""
786
- % (support_heading , stderr , support_closing )
810
+ % (support_heading , stderr , support_closing ),
787
811
)
788
812
return
789
813
elif code != 0 and (
@@ -797,6 +821,7 @@ class and your mirroring bot does not have access to the relevant \
797
821
if options .ignore_expired_tickets :
798
822
return
799
823
send_error_zulip (
824
+ zulip_client ,
800
825
"""%s
801
826
802
827
Your last message was forwarded from Zulip to Zephyr unauthenticated, \
@@ -806,14 +831,15 @@ class and your mirroring bot does not have access to the relevant \
806
831
authenticated Zephyr messages for you again.
807
832
808
833
%s"""
809
- % (support_heading , support_closing )
834
+ % (support_heading , support_closing ),
810
835
)
811
836
return
812
837
813
838
# zwrite failed and it wasn't because of expired tickets: This is
814
839
# probably because the recipient isn't subscribed to personals,
815
840
# but regardless, we should just notify the user.
816
841
send_error_zulip (
842
+ zulip_client ,
817
843
"""%s
818
844
819
845
Your Zulip-Zephyr mirror bot was unable to forward that last message \
@@ -823,12 +849,12 @@ class and your mirroring bot does not have access to the relevant \
823
849
%s
824
850
825
851
%s"""
826
- % (support_heading , stderr , support_closing )
852
+ % (support_heading , stderr , support_closing ),
827
853
)
828
854
return
829
855
830
856
831
- def maybe_forward_to_zephyr (message : Dict [str , Any ]) -> None :
857
+ def maybe_forward_to_zephyr (message : Dict [str , Any ], zulip_client : zulip . Client ) -> None :
832
858
# The key string can be used to direct any type of text.
833
859
if message ["sender_email" ] == zulip_account_email :
834
860
if not (
@@ -851,20 +877,24 @@ def maybe_forward_to_zephyr(message: Dict[str, Any]) -> None:
851
877
)
852
878
return
853
879
try :
854
- forward_to_zephyr (message )
880
+ forward_to_zephyr (message , zulip_client )
855
881
except Exception :
856
882
# Don't let an exception forwarding one message crash the
857
883
# whole process
858
884
logger .exception ("Error forwarding message:" )
859
885
860
886
861
887
def zulip_to_zephyr (options : optparse .Values ) -> NoReturn :
888
+ zulip_client = make_zulip_client ()
889
+
862
890
# Sync messages from zulip to zephyr
863
891
logger .info ("Starting syncing messages." )
864
892
backoff = RandomExponentialBackoff (timeout_success_equivalent = 120 )
865
893
while True :
866
894
try :
867
- zulip_client .call_on_each_message (maybe_forward_to_zephyr )
895
+ zulip_client .call_on_each_message (
896
+ lambda message : maybe_forward_to_zephyr (message , zulip_client )
897
+ )
868
898
except Exception :
869
899
logger .exception ("Error syncing messages:" )
870
900
backoff .fail ()
@@ -886,6 +916,8 @@ def subscribed_to_mail_messages() -> bool:
886
916
887
917
888
918
def add_zulip_subscriptions (verbose : bool ) -> None :
919
+ zulip_client = make_zulip_client ()
920
+
889
921
zephyr_subscriptions = set ()
890
922
skipped = set ()
891
923
for (cls , instance , recipient ) in parse_zephyr_subs (verbose = verbose ):
@@ -1146,7 +1178,7 @@ def parse_args() -> Tuple[optparse.Values, List[str]]:
1146
1178
1147
1179
1148
1180
def die_gracefully (signal : int , frame : FrameType ) -> None :
1149
- if CURRENT_STATE == States .ZulipToZephyr or CURRENT_STATE == States . ChildSending :
1181
+ if CURRENT_STATE == States .ZulipToZephyr :
1150
1182
# this is a child process, so we want os._exit (no clean-up necessary)
1151
1183
os ._exit (1 )
1152
1184
@@ -1207,15 +1239,6 @@ def die_gracefully(signal: int, frame: FrameType) -> None:
1207
1239
sys .exit (1 )
1208
1240
1209
1241
zulip_account_email = options .user + "@mit.edu"
1210
- import zulip
1211
-
1212
- zulip_client = zulip .Client (
1213
- email = zulip_account_email ,
1214
- api_key = api_key ,
1215
- verbose = True ,
1216
- client = "zephyr_mirror" ,
1217
- site = options .site ,
1218
- )
1219
1242
1220
1243
start_time = time .time ()
1221
1244
0 commit comments