zephyr: Attempt to fix types.
The mirror has some chance of running on Python 3 now, once the python-zephyr patch is rebased on 0.2.1, though it’s untested. Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
parent
34012a4015
commit
503e8ed82d
|
@ -9,7 +9,7 @@ import hashlib
|
||||||
import zephyr
|
import zephyr
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
from typing import Any, Dict, List, Set, Tuple
|
from typing import Dict, List, Set, Tuple
|
||||||
|
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
parser.add_option('--verbose',
|
parser.add_option('--verbose',
|
||||||
|
@ -101,11 +101,11 @@ def send_zulip(message: Dict[str, str]) -> None:
|
||||||
# Returns True if and only if we "Detected server failure" sending the zephyr.
|
# Returns True if and only if we "Detected server failure" sending the zephyr.
|
||||||
def send_zephyr(zwrite_args: List[str], content: str) -> bool:
|
def send_zephyr(zwrite_args: List[str], content: str) -> bool:
|
||||||
p = subprocess.Popen(zwrite_args, stdin=subprocess.PIPE,
|
p = subprocess.Popen(zwrite_args, stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
stdout, stderr = p.communicate(input=content.encode("utf-8"))
|
universal_newlines=True)
|
||||||
|
stdout, stderr = p.communicate(input=content)
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
# FIXME: This should really look for a byte form of the string in stdout
|
if "Detected server failure while receiving acknowledgement for" in stdout:
|
||||||
if "Detected server failure while receiving acknowledgement for" in stdout: # type: ignore
|
|
||||||
logger.warning("Got server failure error sending zephyr; retrying")
|
logger.warning("Got server failure error sending zephyr; retrying")
|
||||||
logger.warning(stderr)
|
logger.warning(stderr)
|
||||||
return True
|
return True
|
||||||
|
@ -151,7 +151,7 @@ for tries in range(10):
|
||||||
actually_subscribed = True
|
actually_subscribed = True
|
||||||
break
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if "SERVNAK received" in e: # type: ignore # https://github.com/python/mypy/issues/2118
|
if "SERVNAK received" in e.args:
|
||||||
logger.error("SERVNAK repeatedly received, punting rest of test")
|
logger.error("SERVNAK repeatedly received, punting rest of test")
|
||||||
else:
|
else:
|
||||||
logger.exception("Exception subscribing to zephyrs")
|
logger.exception("Exception subscribing to zephyrs")
|
||||||
|
@ -163,7 +163,7 @@ if not actually_subscribed:
|
||||||
# Prepare keys
|
# Prepare keys
|
||||||
zhkeys = {} # type: Dict[str, Tuple[str, str]]
|
zhkeys = {} # type: Dict[str, Tuple[str, str]]
|
||||||
hzkeys = {} # type: Dict[str, Tuple[str, str]]
|
hzkeys = {} # type: Dict[str, Tuple[str, str]]
|
||||||
def gen_key(key_dict: Dict[str, Any]) -> str:
|
def gen_key(key_dict: Dict[str, Tuple[str, str]]) -> str:
|
||||||
bits = str(random.getrandbits(32))
|
bits = str(random.getrandbits(32))
|
||||||
while bits in key_dict:
|
while bits in key_dict:
|
||||||
# Avoid the unlikely event that we get the same bits twice
|
# Avoid the unlikely event that we get the same bits twice
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from typing import Any, Dict, IO, List, NoReturn, Optional, Set, Text, Tuple, cast
|
from typing import Any, Dict, IO, List, NoReturn, Optional, Set, Tuple, Union
|
||||||
from types import FrameType
|
from types import FrameType
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
@ -17,6 +17,8 @@ import hashlib
|
||||||
import tempfile
|
import tempfile
|
||||||
import select
|
import select
|
||||||
|
|
||||||
|
from typing_extensions import Literal, TypedDict
|
||||||
|
|
||||||
from zulip import RandomExponentialBackoff
|
from zulip import RandomExponentialBackoff
|
||||||
|
|
||||||
DEFAULT_SITE = "https://api.zulip.com"
|
DEFAULT_SITE = "https://api.zulip.com"
|
||||||
|
@ -25,7 +27,7 @@ class States:
|
||||||
Startup, ZulipToZephyr, ZephyrToZulip, ChildSending = list(range(4))
|
Startup, ZulipToZephyr, ZephyrToZulip, ChildSending = list(range(4))
|
||||||
CURRENT_STATE = States.Startup
|
CURRENT_STATE = States.Startup
|
||||||
|
|
||||||
logger = cast(logging.Logger, None) # type: logging.Logger # FIXME cast should not be needed?
|
logger: logging.Logger
|
||||||
|
|
||||||
def to_zulip_username(zephyr_username: str) -> str:
|
def to_zulip_username(zephyr_username: str) -> str:
|
||||||
if "@" in zephyr_username:
|
if "@" in zephyr_username:
|
||||||
|
@ -98,7 +100,18 @@ def unwrap_lines(body: str) -> str:
|
||||||
result += previous_line
|
result += previous_line
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def send_zulip(zeph: Dict[str, str]) -> Dict[str, str]:
|
class ZephyrDict(TypedDict, total=False):
|
||||||
|
type: Literal["private", "stream"]
|
||||||
|
time: str
|
||||||
|
sender: str
|
||||||
|
stream: str
|
||||||
|
subject: str
|
||||||
|
recipient: Union[str, List[str]]
|
||||||
|
content: str
|
||||||
|
zsig: str
|
||||||
|
|
||||||
|
def send_zulip(zeph: ZephyrDict) -> Dict[str, Any]:
|
||||||
|
message: Dict[str, Any]
|
||||||
message = {}
|
message = {}
|
||||||
if options.forward_class_messages:
|
if options.forward_class_messages:
|
||||||
message["forged"] = "yes"
|
message["forged"] = "yes"
|
||||||
|
@ -177,7 +190,7 @@ def zephyr_bulk_subscribe(subs: List[Tuple[str, str, str]]) -> None:
|
||||||
def update_subscriptions() -> None:
|
def update_subscriptions() -> None:
|
||||||
try:
|
try:
|
||||||
f = open(options.stream_file_path)
|
f = open(options.stream_file_path)
|
||||||
public_streams = json.loads(f.read())
|
public_streams: List[str] = json.loads(f.read())
|
||||||
f.close()
|
f.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Error reading public streams:")
|
logger.exception("Error reading public streams:")
|
||||||
|
@ -185,10 +198,10 @@ def update_subscriptions() -> None:
|
||||||
|
|
||||||
classes_to_subscribe = set()
|
classes_to_subscribe = set()
|
||||||
for stream in public_streams:
|
for stream in public_streams:
|
||||||
zephyr_class = stream.encode("utf-8")
|
zephyr_class = stream
|
||||||
if (
|
if (
|
||||||
options.shard is not None
|
options.shard is not None
|
||||||
and not hashlib.sha1(zephyr_class).hexdigest().startswith(options.shard)
|
and not hashlib.sha1(zephyr_class.encode("utf-8")).hexdigest().startswith(options.shard)
|
||||||
):
|
):
|
||||||
# This stream is being handled by a different zephyr_mirror job.
|
# This stream is being handled by a different zephyr_mirror job.
|
||||||
continue
|
continue
|
||||||
|
@ -232,7 +245,7 @@ def maybe_restart_mirroring_script() -> None:
|
||||||
backoff.fail()
|
backoff.fail()
|
||||||
raise Exception("Failed to reload too many times, aborting!")
|
raise Exception("Failed to reload too many times, aborting!")
|
||||||
|
|
||||||
def process_loop(log: Optional[IO[Any]]) -> NoReturn:
|
def process_loop(log: Optional[IO[str]]) -> NoReturn:
|
||||||
restart_check_count = 0
|
restart_check_count = 0
|
||||||
last_check_time = time.time()
|
last_check_time = time.time()
|
||||||
recieve_backoff = RandomExponentialBackoff()
|
recieve_backoff = RandomExponentialBackoff()
|
||||||
|
@ -293,7 +306,7 @@ def parse_zephyr_body(zephyr_data: str, notice_format: str) -> Tuple[str, str]:
|
||||||
body = body.replace('\x00', '')
|
body = body.replace('\x00', '')
|
||||||
return (zsig, body)
|
return (zsig, body)
|
||||||
|
|
||||||
def parse_crypt_table(zephyr_class: Text, instance: str) -> Optional[str]:
|
def parse_crypt_table(zephyr_class: str, instance: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
crypt_table = open(os.path.join(os.environ["HOME"], ".crypt-table"))
|
crypt_table = open(os.path.join(os.environ["HOME"], ".crypt-table"))
|
||||||
except OSError:
|
except OSError:
|
||||||
|
@ -314,7 +327,7 @@ def parse_crypt_table(zephyr_class: Text, instance: str) -> Optional[str]:
|
||||||
return groups["keypath"]
|
return groups["keypath"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def decrypt_zephyr(zephyr_class: Text, instance: str, body: str) -> str:
|
def decrypt_zephyr(zephyr_class: str, instance: str, body: str) -> str:
|
||||||
keypath = parse_crypt_table(zephyr_class, instance)
|
keypath = parse_crypt_table(zephyr_class, instance)
|
||||||
if keypath is None:
|
if keypath is None:
|
||||||
# We can't decrypt it, so we just return the original body
|
# We can't decrypt it, so we just return the original body
|
||||||
|
@ -338,13 +351,16 @@ def decrypt_zephyr(zephyr_class: Text, instance: str, body: str) -> str:
|
||||||
keypath],
|
keypath],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE,
|
||||||
decrypted, _ = p.communicate(input=body) # type: ignore # Optional[bytes] vs string
|
universal_newlines=True,
|
||||||
|
errors="replace")
|
||||||
|
decrypted, _ = p.communicate(input=body)
|
||||||
# Restore our ignoring signals
|
# Restore our ignoring signals
|
||||||
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
||||||
return decrypted # type: ignore # bytes, expecting str
|
return decrypted
|
||||||
|
|
||||||
def process_notice(notice: Any, log: Optional[IO[Any]]) -> None:
|
def process_notice(notice: "zephyr.ZNotice", log: Optional[IO[str]]) -> None:
|
||||||
|
assert notice.sender is not None
|
||||||
(zsig, body) = parse_zephyr_body(notice.message, notice.format)
|
(zsig, body) = parse_zephyr_body(notice.message, notice.format)
|
||||||
is_personal = False
|
is_personal = False
|
||||||
is_huddle = False
|
is_huddle = False
|
||||||
|
@ -392,9 +408,10 @@ def process_notice(notice: Any, log: Optional[IO[Any]]) -> None:
|
||||||
huddle_recipients.append(to_zulip_username(notice.sender))
|
huddle_recipients.append(to_zulip_username(notice.sender))
|
||||||
body = body.split("\n", 1)[1]
|
body = body.split("\n", 1)[1]
|
||||||
|
|
||||||
if options.forward_class_messages and notice.opcode.lower() == "crypt":
|
if options.forward_class_messages and notice.opcode is not None and notice.opcode.lower() == "crypt":
|
||||||
body = decrypt_zephyr(zephyr_class, notice.instance.lower(), body)
|
body = decrypt_zephyr(zephyr_class, notice.instance.lower(), body)
|
||||||
|
|
||||||
|
zeph: ZephyrDict
|
||||||
zeph = {'time': str(notice.time),
|
zeph = {'time': str(notice.time),
|
||||||
'sender': notice.sender,
|
'sender': notice.sender,
|
||||||
'zsig': zsig, # logged here but not used by app
|
'zsig': zsig, # logged here but not used by app
|
||||||
|
@ -403,6 +420,7 @@ def process_notice(notice: Any, log: Optional[IO[Any]]) -> None:
|
||||||
zeph['type'] = 'private'
|
zeph['type'] = 'private'
|
||||||
zeph['recipient'] = huddle_recipients
|
zeph['recipient'] = huddle_recipients
|
||||||
elif is_personal:
|
elif is_personal:
|
||||||
|
assert notice.recipient is not None
|
||||||
zeph['type'] = 'private'
|
zeph['type'] = 'private'
|
||||||
zeph['recipient'] = to_zulip_username(notice.recipient)
|
zeph['recipient'] = to_zulip_username(notice.recipient)
|
||||||
else:
|
else:
|
||||||
|
@ -425,8 +443,6 @@ def process_notice(notice: Any, log: Optional[IO[Any]]) -> None:
|
||||||
heading = ""
|
heading = ""
|
||||||
zeph["content"] = heading + zeph["content"]
|
zeph["content"] = heading + zeph["content"]
|
||||||
|
|
||||||
zeph = decode_unicode_byte_strings(zeph)
|
|
||||||
|
|
||||||
logger.info("Received a message on %s/%s from %s..." %
|
logger.info("Received a message on %s/%s from %s..." %
|
||||||
(zephyr_class, notice.instance, notice.sender))
|
(zephyr_class, notice.instance, notice.sender))
|
||||||
if log is not None:
|
if log is not None:
|
||||||
|
@ -446,17 +462,6 @@ def process_notice(notice: Any, log: Optional[IO[Any]]) -> None:
|
||||||
finally:
|
finally:
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
def decode_unicode_byte_strings(zeph: Dict[str, Any]) -> Dict[str, str]:
|
|
||||||
# 'Any' can be of any type of text that is converted to str.
|
|
||||||
for field in zeph.keys():
|
|
||||||
if isinstance(zeph[field], str):
|
|
||||||
try:
|
|
||||||
decoded = zeph[field].decode("utf-8")
|
|
||||||
except Exception:
|
|
||||||
decoded = zeph[field].decode("iso-8859-1")
|
|
||||||
zeph[field] = decoded
|
|
||||||
return zeph
|
|
||||||
|
|
||||||
def quit_failed_initialization(message: str) -> str:
|
def quit_failed_initialization(message: str) -> str:
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
maybe_kill_child()
|
maybe_kill_child()
|
||||||
|
@ -508,7 +513,7 @@ def zephyr_subscribe_autoretry(sub: Tuple[str, str, str]) -> None:
|
||||||
|
|
||||||
quit_failed_initialization("Could not subscribe to personals, quitting!")
|
quit_failed_initialization("Could not subscribe to personals, quitting!")
|
||||||
|
|
||||||
def zephyr_to_zulip(options: Any) -> None:
|
def zephyr_to_zulip(options: optparse.Values) -> None:
|
||||||
if options.use_sessions and os.path.exists(options.session_path):
|
if options.use_sessions and os.path.exists(options.session_path):
|
||||||
logger.info("Loading old session")
|
logger.info("Loading old session")
|
||||||
zephyr_load_session_autoretry(options.session_path)
|
zephyr_load_session_autoretry(options.session_path)
|
||||||
|
@ -532,12 +537,6 @@ def zephyr_to_zulip(options: Any) -> None:
|
||||||
for ln in log:
|
for ln in log:
|
||||||
try:
|
try:
|
||||||
zeph = json.loads(ln)
|
zeph = json.loads(ln)
|
||||||
# New messages added to the log shouldn't have any
|
|
||||||
# elements of type str (they should already all be
|
|
||||||
# unicode), but older messages in the log are
|
|
||||||
# still of type str, so convert them before we
|
|
||||||
# send the message
|
|
||||||
zeph = decode_unicode_byte_strings(zeph)
|
|
||||||
# Handle importing older zephyrs in the logs
|
# Handle importing older zephyrs in the logs
|
||||||
# where it isn't called a "stream" yet
|
# where it isn't called a "stream" yet
|
||||||
if "class" in zeph:
|
if "class" in zeph:
|
||||||
|
@ -562,19 +561,20 @@ def zephyr_to_zulip(options: Any) -> None:
|
||||||
|
|
||||||
def send_zephyr(zwrite_args: List[str], content: str) -> Tuple[int, str]:
|
def send_zephyr(zwrite_args: List[str], content: str) -> Tuple[int, str]:
|
||||||
p = subprocess.Popen(zwrite_args, stdin=subprocess.PIPE,
|
p = subprocess.Popen(zwrite_args, stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
stdout, stderr = p.communicate(input=content.encode("utf-8"))
|
universal_newlines=True)
|
||||||
|
stdout, stderr = p.communicate(input=content)
|
||||||
if p.returncode:
|
if p.returncode:
|
||||||
logger.error("zwrite command '%s' failed with return code %d:" % (
|
logger.error("zwrite command '%s' failed with return code %d:" % (
|
||||||
" ".join(zwrite_args), p.returncode,))
|
" ".join(zwrite_args), p.returncode,))
|
||||||
if stdout:
|
if stdout:
|
||||||
logger.info("stdout: " + stdout) # type: ignore # str + bytes
|
logger.info("stdout: " + stdout)
|
||||||
elif stderr:
|
elif stderr:
|
||||||
logger.warning("zwrite command '%s' printed the following warning:" % (
|
logger.warning("zwrite command '%s' printed the following warning:" % (
|
||||||
" ".join(zwrite_args),))
|
" ".join(zwrite_args),))
|
||||||
if stderr:
|
if stderr:
|
||||||
logger.warning("stderr: " + stderr) # type: ignore # str + bytes
|
logger.warning("stderr: " + stderr)
|
||||||
return (p.returncode, stderr) # type: ignore # bytes vs str
|
return (p.returncode, stderr)
|
||||||
|
|
||||||
def send_authed_zephyr(zwrite_args: List[str], content: str) -> Tuple[int, str]:
|
def send_authed_zephyr(zwrite_args: List[str], content: str) -> Tuple[int, str]:
|
||||||
return send_zephyr(zwrite_args, content)
|
return send_zephyr(zwrite_args, content)
|
||||||
|
@ -603,9 +603,10 @@ def zcrypt_encrypt_content(zephyr_class: str, instance: str, content: str) -> Op
|
||||||
keypath],
|
keypath],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE,
|
||||||
encrypted, _ = p.communicate(input=content) # type: ignore # Optional[bytes] vs string
|
universal_newlines=True)
|
||||||
return encrypted # type: ignore # bytes, expecting Optional[str]
|
encrypted, _ = p.communicate(input=content)
|
||||||
|
return encrypted
|
||||||
|
|
||||||
def forward_to_zephyr(message: Dict[str, Any]) -> None:
|
def forward_to_zephyr(message: Dict[str, Any]) -> None:
|
||||||
# 'Any' can be of any type of text
|
# 'Any' can be of any type of text
|
||||||
|
@ -688,8 +689,7 @@ Zulip users (like you) received it, Zephyr users did not.
|
||||||
|
|
||||||
if options.test_mode:
|
if options.test_mode:
|
||||||
logger.debug("Would have forwarded: %s\n%s" %
|
logger.debug("Would have forwarded: %s\n%s" %
|
||||||
(zwrite_args, wrapped_content.encode("utf-8"))) # type: ignore
|
(zwrite_args, wrapped_content))
|
||||||
# NOTE: mypy indicates %s outputs the encoded wrapped_content as per %r
|
|
||||||
return
|
return
|
||||||
|
|
||||||
(code, stderr) = send_authed_zephyr(zwrite_args, wrapped_content)
|
(code, stderr) = send_authed_zephyr(zwrite_args, wrapped_content)
|
||||||
|
@ -769,7 +769,7 @@ def maybe_forward_to_zephyr(message: Dict[str, Any]) -> None:
|
||||||
# whole process
|
# whole process
|
||||||
logger.exception("Error forwarding message:")
|
logger.exception("Error forwarding message:")
|
||||||
|
|
||||||
def zulip_to_zephyr(options: int) -> NoReturn:
|
def zulip_to_zephyr(options: optparse.Values) -> NoReturn:
|
||||||
# Sync messages from zulip to zephyr
|
# Sync messages from zulip to zephyr
|
||||||
logger.info("Starting syncing messages.")
|
logger.info("Starting syncing messages.")
|
||||||
backoff = RandomExponentialBackoff(timeout_success_equivalent=120)
|
backoff = RandomExponentialBackoff(timeout_success_equivalent=120)
|
||||||
|
@ -950,7 +950,7 @@ def configure_logger(logger: logging.Logger, direction_name: Optional[str]) -> N
|
||||||
for handler in root_logger.handlers:
|
for handler in root_logger.handlers:
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
def parse_args() -> Tuple[Any, ...]:
|
def parse_args() -> Tuple[optparse.Values, List[str]]:
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
parser.add_option('--forward-class-messages',
|
parser.add_option('--forward-class-messages',
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -1059,10 +1059,7 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, die_gracefully)
|
signal.signal(signal.SIGINT, die_gracefully)
|
||||||
|
|
||||||
# The properties available on 'options' are dynamically
|
(options, args) = parse_args()
|
||||||
# determined, so we have to treat it as an Any for type
|
|
||||||
# annotations.
|
|
||||||
(options, args) = parse_args() # type: Any, List[str]
|
|
||||||
|
|
||||||
logger = open_logger()
|
logger = open_logger()
|
||||||
configure_logger(logger, "parent")
|
configure_logger(logger, "parent")
|
||||||
|
|
Loading…
Reference in a new issue