refactor(meshchat): implement delayed hangup for rejected calls and improve contact lookup handling

This commit is contained in:
Ivan
2026-05-09 00:56:33 -05:00
parent 6bc49c4355
commit 9f04aef66a
3 changed files with 73 additions and 19 deletions
+43 -13
View File
@@ -2932,8 +2932,12 @@ class ReticulumMeshChat:
# Reject all calls if telephony is disabled
if not ctx.config.telephone_enabled.get():
if ctx.telephone_manager.telephone:
ctx.telephone_manager.telephone.hangup()
telephone = getattr(ctx.telephone_manager, "telephone", None)
if telephone:
threading.Timer(
0.5,
lambda t=telephone: t.hangup(),
).start()
return
if ctx.telephone_manager and ctx.telephone_manager.initiation_status:
@@ -2947,18 +2951,24 @@ class ReticulumMeshChat:
# Check if caller is blocked
if self.is_destination_blocked(caller_hash, context=ctx):
print(f"Rejecting incoming call from blocked source: {caller_hash}")
if ctx.telephone_manager.telephone:
ctx.telephone_manager.telephone.hangup()
telephone = getattr(ctx.telephone_manager, "telephone", None)
if telephone:
# Use a small delay to avoid deadlocking with LXST call_handler_lock
threading.Timer(
0.5,
lambda t=telephone: t.hangup(),
).start()
return
# Check for Do Not Disturb
if ctx.config.do_not_disturb_enabled.get():
print(f"Rejecting incoming call due to Do Not Disturb: {caller_hash}")
if ctx.telephone_manager.telephone:
telephone = getattr(ctx.telephone_manager, "telephone", None)
if telephone:
# Use a small delay to ensure LXST state is ready for hangup
threading.Timer(
0.5,
lambda: ctx.telephone_manager.telephone.hangup(),
lambda t=telephone: t.hangup(),
).start()
return
@@ -2967,13 +2977,21 @@ class ReticulumMeshChat:
ctx.config.telephone_allow_calls_from_contacts_only.get()
or ctx.config.block_all_from_strangers.get()
):
contact = ctx.database.contacts.get_contact_by_identity_hash(caller_hash)
contact = None
try:
contact = ctx.database.contacts.get_contact_by_identity_hash(
caller_hash
)
except Exception:
# Treat lookup failure as non-contact to avoid accidentally allowing spam
pass
if not contact:
print(f"Rejecting incoming call from non-contact: {caller_hash}")
if ctx.telephone_manager.telephone:
telephone = getattr(ctx.telephone_manager, "telephone", None)
if telephone:
threading.Timer(
0.5,
lambda: ctx.telephone_manager.telephone.hangup(),
lambda t=telephone: t.hangup(),
).start()
return
@@ -2983,6 +3001,13 @@ class ReticulumMeshChat:
print(f"on_incoming_telephone_call: {caller_identity.hash.hex()}")
ch = caller_identity.hash.hex()
caller_name = (self.get_name_for_identity_hash(ch) or "").strip() or "Mesh"
is_contact = False
try:
is_contact = (
ctx.database.contacts.get_contact_by_identity_hash(ch) is not None
)
except Exception:
pass
AsyncUtils.run_async(
self.websocket_broadcast(
json.dumps(
@@ -2990,6 +3015,7 @@ class ReticulumMeshChat:
"type": "telephone_ringing",
"remote_identity_hash": ch,
"remote_identity_name": caller_name,
"is_contact": is_contact,
},
),
),
@@ -3079,10 +3105,14 @@ class ReticulumMeshChat:
if ctx.config.do_not_disturb_enabled.get():
is_filtered = True
elif ctx.config.telephone_allow_calls_from_contacts_only.get():
contact = ctx.database.contacts.get_contact_by_identity_hash(
remote_identity_hash,
)
if not contact:
try:
contact = ctx.database.contacts.get_contact_by_identity_hash(
remote_identity_hash,
)
if not contact:
is_filtered = True
except Exception:
# Treat lookup failure as filtered to avoid leaking missed-call noise
is_filtered = True
if not is_filtered:
+3 -1
View File
@@ -1174,7 +1174,9 @@ export default {
if (this.config?.do_not_disturb_enabled) {
break;
}
// If we are the caller (outgoing initiation), skip playing the incoming ringtone
if (this.config?.telephone_allow_calls_from_contacts_only && !json.is_contact) {
break;
}
if (this.initiationStatus) {
break;
}
+27 -5
View File
@@ -39,10 +39,22 @@ def _run_incoming(app, caller, ctx=None):
bound(caller, context=ctx)
def test_incoming_rejects_when_blocked_immediate_hangup(policy_app):
def test_incoming_rejects_when_blocked_uses_delayed_hangup(policy_app):
policy_app.is_destination_blocked.return_value = True
caller = _caller_identity()
_run_incoming(policy_app, caller)
with patch("meshchatx.meshchat.threading.Timer") as mock_timer:
def run_timer(delay, fn):
assert delay == 0.5
fn()
t = MagicMock()
t.start = MagicMock()
return t
mock_timer.side_effect = run_timer
_run_incoming(policy_app, caller)
policy_app.telephone_manager.telephone.hangup.assert_called_once()
policy_app.voicemail_manager.handle_incoming_call.assert_not_called()
@@ -127,9 +139,14 @@ def test_contacts_only_accepts_matching_contact(policy_app):
async_utils.run_async = MagicMock()
_run_incoming(policy_app, caller)
policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_called_once_with(
# Called twice: once for policy check, once for websocket broadcast is_contact flag
policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_called_with(
CALLER_HASH_HEX,
)
assert (
policy_app.current_context.database.contacts.get_contact_by_identity_hash.call_count
== 2
)
policy_app.current_context.voicemail_manager.handle_incoming_call.assert_called_once_with(
caller
)
@@ -178,7 +195,10 @@ def test_when_policy_off_stranger_rings(policy_app):
async_utils.run_async = MagicMock()
_run_incoming(policy_app, caller)
policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_not_called()
# Called once for websocket broadcast is_contact flag even when policy is off
policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_called_once_with(
CALLER_HASH_HEX,
)
policy_app.current_context.voicemail_manager.handle_incoming_call.assert_called_once_with(
caller
)
@@ -224,8 +244,10 @@ def test_uses_passed_context_not_app_current_context(policy_app):
async_utils.run_async = MagicMock()
_run_incoming(policy_app, caller, ctx=other_ctx)
other_ctx.database.contacts.get_contact_by_identity_hash.assert_called_once_with(
# Called twice: once for policy check, once for websocket broadcast is_contact flag
other_ctx.database.contacts.get_contact_by_identity_hash.assert_called_with(
CALLER_HASH_HEX,
)
assert other_ctx.database.contacts.get_contact_by_identity_hash.call_count == 2
policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_not_called()
other_ctx.voicemail_manager.handle_incoming_call.assert_called_once_with(caller)