mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-07-09 08:11:41 +00:00
refactor(meshchat): implement delayed hangup for rejected calls and improve contact lookup handling
This commit is contained in:
+43
-13
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user