feat(call): improve call handling by adding remote destination hash tracking, improving initiation status checks, and refining ringtone management in the frontend
This commit is contained in:
@@ -1650,6 +1650,13 @@ class ReticulumMeshChat:
|
|||||||
ctx = context or self.current_context
|
ctx = context or self.current_context
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if ctx.telephone_manager and ctx.telephone_manager.initiation_status:
|
||||||
|
print(
|
||||||
|
"on_incoming_telephone_call: Ignoring as we are currently initiating an outgoing call."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
caller_hash = caller_identity.hash.hex()
|
caller_hash = caller_identity.hash.hex()
|
||||||
|
|
||||||
# Check if caller is blocked
|
# Check if caller is blocked
|
||||||
@@ -3872,6 +3879,9 @@ class ReticulumMeshChat:
|
|||||||
if telephone_active_call is not None:
|
if telephone_active_call is not None:
|
||||||
# remote_identity is already fetched and checked for None above
|
# remote_identity is already fetched and checked for None above
|
||||||
remote_hash = remote_identity.hash.hex()
|
remote_hash = remote_identity.hash.hex()
|
||||||
|
remote_destination_hash = RNS.Destination.hash(
|
||||||
|
remote_identity, "lxmf", "delivery"
|
||||||
|
).hex()
|
||||||
remote_name = None
|
remote_name = None
|
||||||
if self.telephone_manager.get_name_for_identity_hash:
|
if self.telephone_manager.get_name_for_identity_hash:
|
||||||
remote_name = self.telephone_manager.get_name_for_identity_hash(
|
remote_name = self.telephone_manager.get_name_for_identity_hash(
|
||||||
@@ -3896,6 +3906,7 @@ class ReticulumMeshChat:
|
|||||||
active_call = {
|
active_call = {
|
||||||
"hash": telephone_active_call.hash.hex(),
|
"hash": telephone_active_call.hash.hex(),
|
||||||
"remote_identity_hash": remote_hash,
|
"remote_identity_hash": remote_hash,
|
||||||
|
"remote_destination_hash": remote_destination_hash,
|
||||||
"remote_identity_name": remote_name,
|
"remote_identity_name": remote_name,
|
||||||
"remote_icon": dict(remote_icon) if remote_icon else None,
|
"remote_icon": dict(remote_icon) if remote_icon else None,
|
||||||
"custom_image": custom_image,
|
"custom_image": custom_image,
|
||||||
@@ -4068,6 +4079,7 @@ class ReticulumMeshChat:
|
|||||||
remote_identity_hash,
|
remote_identity_hash,
|
||||||
)
|
)
|
||||||
if lxmf_hash:
|
if lxmf_hash:
|
||||||
|
d["remote_destination_hash"] = lxmf_hash
|
||||||
icon = self.database.misc.get_user_icon(lxmf_hash)
|
icon = self.database.misc.get_user_icon(lxmf_hash)
|
||||||
if icon:
|
if icon:
|
||||||
d["remote_icon"] = dict(icon)
|
d["remote_icon"] = dict(icon)
|
||||||
@@ -4238,6 +4250,7 @@ class ReticulumMeshChat:
|
|||||||
remote_identity_hash,
|
remote_identity_hash,
|
||||||
)
|
)
|
||||||
if lxmf_hash:
|
if lxmf_hash:
|
||||||
|
d["remote_destination_hash"] = lxmf_hash
|
||||||
icon = self.database.misc.get_user_icon(lxmf_hash)
|
icon = self.database.misc.get_user_icon(lxmf_hash)
|
||||||
if icon:
|
if icon:
|
||||||
d["remote_icon"] = dict(icon)
|
d["remote_icon"] = dict(icon)
|
||||||
@@ -4296,7 +4309,9 @@ class ReticulumMeshChat:
|
|||||||
try:
|
try:
|
||||||
voicemail_id = int(voicemail_id)
|
voicemail_id = int(voicemail_id)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return web.json_response({"message": "Invalid voicemail ID"}, status=400)
|
return web.json_response(
|
||||||
|
{"message": "Invalid voicemail ID"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
if not self.voicemail_manager:
|
if not self.voicemail_manager:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -9536,7 +9551,15 @@ class ReticulumMeshChat:
|
|||||||
"Received an announce from "
|
"Received an announce from "
|
||||||
+ RNS.prettyhexrep(destination_hash)
|
+ RNS.prettyhexrep(destination_hash)
|
||||||
+ " for [lxst.telephony]"
|
+ " for [lxst.telephony]"
|
||||||
+ (f" ({display_name})" if (display_name := parse_lxmf_display_name(base64.b64encode(app_data).decode() if app_data else None, None)) else "")
|
+ (
|
||||||
|
f" ({display_name})"
|
||||||
|
if (
|
||||||
|
display_name := parse_lxmf_display_name(
|
||||||
|
base64.b64encode(app_data).decode() if app_data else None, None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else ""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# track announce timestamp
|
# track announce timestamp
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class TelephoneManager:
|
|||||||
self.telephone.hangup()
|
self.telephone.hangup()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"TelephoneManager: Error during hangup: {e}", RNS.LOG_ERROR)
|
RNS.log(f"TelephoneManager: Error during hangup: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
# Always clear initiation status on hangup to prevent "Dialing..." hang
|
# Always clear initiation status on hangup to prevent "Dialing..." hang
|
||||||
self._update_initiation_status(None, None)
|
self._update_initiation_status(None, None)
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ class TelephoneManager:
|
|||||||
# Update start time to when it was actually established for duration calculation
|
# Update start time to when it was actually established for duration calculation
|
||||||
self.call_start_time = time.time()
|
self.call_start_time = time.time()
|
||||||
self.call_was_established = True
|
self.call_was_established = True
|
||||||
|
|
||||||
# Clear initiation status as soon as call is established
|
# Clear initiation status as soon as call is established
|
||||||
self._update_initiation_status(None, None)
|
self._update_initiation_status(None, None)
|
||||||
|
|
||||||
@@ -296,21 +296,43 @@ class TelephoneManager:
|
|||||||
self._update_initiation_status("Dialing...")
|
self._update_initiation_status("Dialing...")
|
||||||
self.call_start_time = time.time()
|
self.call_start_time = time.time()
|
||||||
self.call_is_incoming = False
|
self.call_is_incoming = False
|
||||||
|
|
||||||
# Use a thread for the blocking LXST call, but monitor status for early exit
|
# Use a thread for the blocking LXST call, but monitor status for early exit
|
||||||
# if established elsewhere or timed out/hung up
|
# if established elsewhere or timed out/hung up
|
||||||
call_task = asyncio.create_task(asyncio.to_thread(self.telephone.call, destination_identity))
|
call_task = asyncio.create_task(
|
||||||
|
asyncio.to_thread(self.telephone.call, destination_identity)
|
||||||
|
)
|
||||||
|
|
||||||
start_wait = time.time()
|
start_wait = time.time()
|
||||||
# LXST telephone.call usually returns on establishment or timeout.
|
# LXST telephone.call usually returns on establishment or timeout.
|
||||||
# We wait for it, but if status becomes established or ended, we can stop waiting.
|
# We wait for it, but if status becomes established or ended, we can stop waiting.
|
||||||
while not call_task.done():
|
while not call_task.done():
|
||||||
if self.telephone.call_status in [6, 0, 1]: # Established, Busy, Rejected
|
if self.telephone.call_status in [
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
]: # Established, Busy, Rejected
|
||||||
break
|
break
|
||||||
if self.telephone.call_status == 3 and (time.time() - start_wait > 1.0): # Available (ended/timeout)
|
if self.telephone.call_status == 3 and (
|
||||||
|
time.time() - start_wait > 1.0
|
||||||
|
): # Available (ended/timeout)
|
||||||
break
|
break
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# If the task finished but we're still ringing or connecting,
|
||||||
|
# wait a bit more for establishment or definitive failure
|
||||||
|
if self.telephone.call_status in [4, 5]: # Ringing, Connecting
|
||||||
|
wait_until = time.time() + timeout_seconds
|
||||||
|
while time.time() < wait_until:
|
||||||
|
if self.telephone.call_status in [
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
]: # Established, Busy, Rejected, Ended
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
return self.telephone.active_call
|
return self.telephone.active_call
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -550,14 +550,14 @@ class VoicemailManager:
|
|||||||
"1",
|
"1",
|
||||||
temp_path,
|
temp_path,
|
||||||
]
|
]
|
||||||
result = subprocess.run(
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False) # noqa: S603
|
||||||
cmd, capture_output=True, text=True, check=False
|
|
||||||
) # noqa: S603
|
|
||||||
|
|
||||||
if result.returncode == 0 and os.path.exists(temp_path):
|
if result.returncode == 0 and os.path.exists(temp_path):
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
os.rename(temp_path, filepath)
|
os.rename(temp_path, filepath)
|
||||||
RNS.log(f"Voicemail: Fixed recording format for {filepath}", RNS.LOG_DEBUG)
|
RNS.log(
|
||||||
|
f"Voicemail: Fixed recording format for {filepath}", RNS.LOG_DEBUG
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"Voicemail: ffmpeg failed to fix {filepath}: {result.stderr}",
|
f"Voicemail: ffmpeg failed to fix {filepath}: {result.stderr}",
|
||||||
@@ -581,7 +581,7 @@ class VoicemailManager:
|
|||||||
"-f",
|
"-f",
|
||||||
"lavfi",
|
"lavfi",
|
||||||
"-i",
|
"-i",
|
||||||
f"anullsrc=r=48000:cl=mono",
|
"anullsrc=r=48000:cl=mono",
|
||||||
"-t",
|
"-t",
|
||||||
str(max(1, seconds)),
|
str(max(1, seconds)),
|
||||||
"-c:a",
|
"-c:a",
|
||||||
@@ -598,7 +598,10 @@ class VoicemailManager:
|
|||||||
RNS.LOG_ERROR,
|
RNS.LOG_ERROR,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"Voicemail: Error creating silence file for {filepath}: {e}", RNS.LOG_ERROR)
|
RNS.log(
|
||||||
|
f"Voicemail: Error creating silence file for {filepath}: {e}",
|
||||||
|
RNS.LOG_ERROR,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def start_greeting_recording(self):
|
def start_greeting_recording(self):
|
||||||
|
|||||||
@@ -724,6 +724,10 @@ export default {
|
|||||||
if (this.config?.do_not_disturb_enabled) {
|
if (this.config?.do_not_disturb_enabled) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// If we are the caller (outgoing initiation), skip playing the incoming ringtone
|
||||||
|
if (this.initiationStatus) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
NotificationUtils.showIncomingCallNotification();
|
NotificationUtils.showIncomingCallNotification();
|
||||||
this.updateTelephoneStatus();
|
this.updateTelephoneStatus();
|
||||||
this.playRingtone();
|
this.playRingtone();
|
||||||
@@ -1059,13 +1063,32 @@ export default {
|
|||||||
this.initiationTargetHash = response.data.initiation_target_hash;
|
this.initiationTargetHash = response.data.initiation_target_hash;
|
||||||
this.initiationTargetName = response.data.initiation_target_name;
|
this.initiationTargetName = response.data.initiation_target_name;
|
||||||
|
|
||||||
|
// Update call ended state if needed
|
||||||
|
const justEnded = oldCall != null && this.activeCall == null;
|
||||||
|
if (justEnded) {
|
||||||
|
this.lastCall = oldCall;
|
||||||
|
this.toneGenerator.playBusyTone();
|
||||||
|
|
||||||
|
// Trigger history refresh
|
||||||
|
GlobalEmitter.emit("telephone-history-updated");
|
||||||
|
|
||||||
|
if (!this.wasDeclined) {
|
||||||
|
this.isCallEnded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
||||||
|
this.endedTimeout = setTimeout(() => {
|
||||||
|
this.isCallEnded = false;
|
||||||
|
this.wasDeclined = false;
|
||||||
|
this.lastCall = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle outgoing ringback tone
|
// Handle outgoing ringback tone
|
||||||
if (this.initiationStatus === "Ringing...") {
|
if (this.initiationStatus === "Ringing...") {
|
||||||
this.toneGenerator.playRingback();
|
this.toneGenerator.playRingback();
|
||||||
} else if (!this.initiationStatus && !this.activeCall) {
|
} else if (!this.initiationStatus && !this.activeCall && !this.isCallEnded) {
|
||||||
// Only stop if we're not ringing or in a call
|
// Only stop if we're not ringing, in a call, or just finished a call (busy tone playing)
|
||||||
// This might be too aggressive if called during a transition,
|
|
||||||
// but toneGenerator.stop() is safe to call multiple times.
|
|
||||||
this.toneGenerator.stop();
|
this.toneGenerator.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,8 +1115,8 @@ export default {
|
|||||||
this.isCallWindowOpen = false;
|
this.isCallWindowOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle ringtone
|
// Handle ringtone (only for incoming ringing)
|
||||||
if (this.activeCall?.status === 4) {
|
if (this.activeCall?.status === 4 && this.activeCall?.is_incoming) {
|
||||||
// Call is ringing
|
// Call is ringing
|
||||||
if (!this.ringtonePlayer && this.config?.custom_ringtone_enabled && !this.isFetchingRingtone) {
|
if (!this.ringtonePlayer && this.config?.custom_ringtone_enabled && !this.isFetchingRingtone) {
|
||||||
this.isFetchingRingtone = true;
|
this.isFetchingRingtone = true;
|
||||||
@@ -1138,25 +1161,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If call just ended, show ended state for a few seconds
|
// If call just ended, show ended state for a few seconds
|
||||||
if (oldCall != null && this.activeCall == null) {
|
if (justEnded) {
|
||||||
this.lastCall = oldCall;
|
// Handled above
|
||||||
this.toneGenerator.playBusyTone();
|
|
||||||
|
|
||||||
// Trigger history refresh
|
|
||||||
GlobalEmitter.emit("telephone-history-updated");
|
|
||||||
|
|
||||||
if (this.wasDeclined) {
|
|
||||||
// Already set by hangupCall
|
|
||||||
} else {
|
|
||||||
this.isCallEnded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
|
||||||
this.endedTimeout = setTimeout(() => {
|
|
||||||
this.isCallEnded = false;
|
|
||||||
this.wasDeclined = false;
|
|
||||||
this.lastCall = null;
|
|
||||||
}, 5000);
|
|
||||||
} else if (this.activeCall != null) {
|
} else if (this.activeCall != null) {
|
||||||
// if a new call starts, clear ended state
|
// if a new call starts, clear ended state
|
||||||
this.isCallEnded = false;
|
this.isCallEnded = false;
|
||||||
|
|||||||
@@ -191,7 +191,9 @@
|
|||||||
{{ $t("call.recording_voicemail") }}
|
{{ $t("call.recording_voicemail") }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="activeCall && activeCall.is_incoming && activeCall.status === 4"
|
v-else-if="
|
||||||
|
activeCall && activeCall.is_incoming && activeCall.status === 4
|
||||||
|
"
|
||||||
class="text-blue-600 dark:text-blue-400 font-bold text-sm animate-bounce"
|
class="text-blue-600 dark:text-blue-400 font-bold text-sm animate-bounce"
|
||||||
>{{ $t("call.incoming_call") }}</span
|
>{{ $t("call.incoming_call") }}</span
|
||||||
>
|
>
|
||||||
@@ -200,17 +202,25 @@
|
|||||||
class="text-gray-700 dark:text-zinc-300 font-bold text-sm flex items-center gap-2"
|
class="text-gray-700 dark:text-zinc-300 font-bold text-sm flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span v-if="activeCall && activeCall.status === 0">Busy...</span>
|
<span v-if="activeCall && activeCall.status === 0">Busy...</span>
|
||||||
<span v-else-if="activeCall && activeCall.status === 1" class="text-red-500"
|
<span
|
||||||
|
v-else-if="activeCall && activeCall.status === 1"
|
||||||
|
class="text-red-500"
|
||||||
>Rejected</span
|
>Rejected</span
|
||||||
>
|
>
|
||||||
<span v-else-if="activeCall && activeCall.status === 2" class="animate-pulse"
|
<span
|
||||||
|
v-else-if="activeCall && activeCall.status === 2"
|
||||||
|
class="animate-pulse"
|
||||||
>Calling...</span
|
>Calling...</span
|
||||||
>
|
>
|
||||||
<span v-else-if="activeCall && activeCall.status === 3">Available</span>
|
<span v-else-if="activeCall && activeCall.status === 3">Available</span>
|
||||||
<span v-else-if="activeCall && activeCall.status === 4" class="animate-pulse"
|
<span
|
||||||
|
v-else-if="activeCall && activeCall.status === 4"
|
||||||
|
class="animate-pulse"
|
||||||
>Ringing...</span
|
>Ringing...</span
|
||||||
>
|
>
|
||||||
<span v-else-if="activeCall && activeCall.status === 5">Connecting...</span>
|
<span v-else-if="activeCall && activeCall.status === 5"
|
||||||
|
>Connecting...</span
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-else-if="activeCall && activeCall.status === 6"
|
v-else-if="activeCall && activeCall.status === 6"
|
||||||
class="text-green-500 flex items-center gap-2"
|
class="text-green-500 flex items-center gap-2"
|
||||||
@@ -651,10 +661,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-[10px] font-mono text-gray-400 dark:text-zinc-600 truncate mt-0.5 cursor-pointer hover:text-blue-500 transition-colors"
|
class="text-[10px] font-mono text-gray-400 dark:text-zinc-600 truncate mt-0.5 cursor-pointer hover:text-blue-500 transition-colors"
|
||||||
:title="entry.remote_identity_hash"
|
:title="entry.remote_destination_hash || entry.remote_identity_hash"
|
||||||
@click.stop="copyHash(entry.remote_identity_hash)"
|
@click.stop="copyHash(entry.remote_destination_hash || entry.remote_identity_hash)"
|
||||||
>
|
>
|
||||||
{{ formatDestinationHash(entry.remote_identity_hash) }}
|
{{ formatDestinationHash(entry.remote_destination_hash || entry.remote_identity_hash) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1239,7 +1249,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="text-[10px] flex items-center gap-1 text-gray-500 hover:text-blue-500 font-bold uppercase tracking-wider transition-colors"
|
class="text-[10px] flex items-center gap-1 text-gray-500 hover:text-blue-500 font-bold uppercase tracking-wider transition-colors"
|
||||||
@click="
|
@click="
|
||||||
destinationHash = voicemail.remote_identity_hash;
|
destinationHash = voicemail.remote_destination_hash || voicemail.remote_identity_hash;
|
||||||
activeTab = 'phone';
|
activeTab = 'phone';
|
||||||
call(destinationHash);
|
call(destinationHash);
|
||||||
"
|
"
|
||||||
@@ -2050,7 +2060,7 @@ export default {
|
|||||||
) {
|
) {
|
||||||
suggestions.push({
|
suggestions.push({
|
||||||
name: c.name,
|
name: c.name,
|
||||||
hash: c.remote_identity_hash,
|
hash: c.remote_destination_hash || c.remote_identity_hash,
|
||||||
type: "contact",
|
type: "contact",
|
||||||
icon: "account",
|
icon: "account",
|
||||||
});
|
});
|
||||||
@@ -2069,7 +2079,7 @@ export default {
|
|||||||
) {
|
) {
|
||||||
suggestions.push({
|
suggestions.push({
|
||||||
name: h.remote_identity_name || h.remote_identity_hash.substring(0, 8),
|
name: h.remote_identity_name || h.remote_identity_hash.substring(0, 8),
|
||||||
hash: h.remote_identity_hash,
|
hash: h.remote_destination_hash || h.remote_identity_hash,
|
||||||
type: "history",
|
type: "history",
|
||||||
icon: "history",
|
icon: "history",
|
||||||
});
|
});
|
||||||
@@ -2199,6 +2209,7 @@ export default {
|
|||||||
const response = await window.axios.get("/api/v1/telephone/status");
|
const response = await window.axios.get("/api/v1/telephone/status");
|
||||||
const oldCall = this.activeCall;
|
const oldCall = this.activeCall;
|
||||||
const newCall = response.data.active_call;
|
const newCall = response.data.active_call;
|
||||||
|
const callStatus = response.data.call_status;
|
||||||
|
|
||||||
// Sync local mute state from backend
|
// Sync local mute state from backend
|
||||||
if (newCall) {
|
if (newCall) {
|
||||||
@@ -2213,6 +2224,14 @@ export default {
|
|||||||
this.initiationTargetHash = response.data.initiation_target_hash;
|
this.initiationTargetHash = response.data.initiation_target_hash;
|
||||||
this.initiationTargetName = response.data.initiation_target_name;
|
this.initiationTargetName = response.data.initiation_target_name;
|
||||||
|
|
||||||
|
// If no active call and status is idle/busy/rejected/available, clear stale initiation UI
|
||||||
|
const isIdleState = !this.activeCall && ![2, 4, 5].includes(callStatus);
|
||||||
|
if (isIdleState && this.initiationStatus) {
|
||||||
|
this.initiationStatus = null;
|
||||||
|
this.initiationTargetHash = null;
|
||||||
|
this.initiationTargetName = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.activeCall?.is_voicemail) {
|
if (this.activeCall?.is_voicemail) {
|
||||||
this.wasVoicemail = true;
|
this.wasVoicemail = true;
|
||||||
}
|
}
|
||||||
@@ -2260,7 +2279,7 @@ export default {
|
|||||||
this.editingContact = null;
|
this.editingContact = null;
|
||||||
this.contactForm = {
|
this.contactForm = {
|
||||||
name: entry.remote_identity_name || "",
|
name: entry.remote_identity_name || "",
|
||||||
remote_identity_hash: entry.remote_identity_hash,
|
remote_identity_hash: entry.remote_destination_hash || entry.remote_identity_hash,
|
||||||
preferred_ringtone_id: null,
|
preferred_ringtone_id: null,
|
||||||
};
|
};
|
||||||
this.isContactModalOpen = true;
|
this.isContactModalOpen = true;
|
||||||
@@ -2863,6 +2882,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hashToCall = identityHash.trim();
|
let hashToCall = identityHash.trim();
|
||||||
|
// Accept lxmf:// URIs or pasted text; extract first 64-char hex
|
||||||
|
const hexMatch = hashToCall.match(/[0-9a-fA-F]{64}/);
|
||||||
|
if (hexMatch) {
|
||||||
|
hashToCall = hexMatch[0];
|
||||||
|
}
|
||||||
|
hashToCall = hashToCall.toLowerCase();
|
||||||
|
|
||||||
// Try to resolve name from contacts
|
// Try to resolve name from contacts
|
||||||
const contact = this.contacts.find((c) => c.name.toLowerCase() === hashToCall.toLowerCase());
|
const contact = this.contacts.find((c) => c.name.toLowerCase() === hashToCall.toLowerCase());
|
||||||
|
|||||||
@@ -1082,8 +1082,12 @@
|
|||||||
class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||||
@click.self="isRawMessageModalOpen = false"
|
@click.self="isRawMessageModalOpen = false"
|
||||||
>
|
>
|
||||||
<div class="w-full max-w-2xl bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
<div
|
||||||
<div class="px-6 py-4 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-between shrink-0">
|
class="w-full max-w-2xl bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="px-6 py-4 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-between shrink-0"
|
||||||
|
>
|
||||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Raw LXMF Message</h3>
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Raw LXMF Message</h3>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1094,7 +1098,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 overflow-y-auto font-mono text-xs bg-gray-50 dark:bg-black/40">
|
<div class="p-6 overflow-y-auto font-mono text-xs bg-gray-50 dark:bg-black/40">
|
||||||
<pre class="whitespace-pre-wrap break-all text-gray-800 dark:text-zinc-300">{{ JSON.stringify(rawMessageData, null, 2) }}</pre>
|
<pre class="whitespace-pre-wrap break-all text-gray-800 dark:text-zinc-300">{{
|
||||||
|
JSON.stringify(rawMessageData, null, 2)
|
||||||
|
}}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 py-4 border-t border-gray-100 dark:border-zinc-800 flex justify-end shrink-0">
|
<div class="px-6 py-4 border-t border-gray-100 dark:border-zinc-800 flex justify-end shrink-0">
|
||||||
<button
|
<button
|
||||||
@@ -1607,10 +1613,7 @@ export default {
|
|||||||
// if content is only the paper message, or it already contains the detected text,
|
// if content is only the paper message, or it already contains the detected text,
|
||||||
// we'll hide the raw content div to avoid double rendering.
|
// we'll hide the raw content div to avoid double rendering.
|
||||||
const trimmedContent = content.trim();
|
const trimmedContent = content.trim();
|
||||||
if (
|
if (trimmedContent === items.paperMessage || trimmedContent.includes("Paper Message detected")) {
|
||||||
trimmedContent === items.paperMessage ||
|
|
||||||
trimmedContent.includes("Paper Message detected")
|
|
||||||
) {
|
|
||||||
items.isOnlyPaperMessage = true;
|
items.isOnlyPaperMessage = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2040,7 +2043,7 @@ export default {
|
|||||||
...chatItem.lxmf_message,
|
...chatItem.lxmf_message,
|
||||||
raw_uri: response.data.uri,
|
raw_uri: response.data.uri,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch {
|
||||||
// if URI is not available (message no longer in router), we show what we have
|
// if URI is not available (message no longer in router), we show what we have
|
||||||
this.rawMessageData = { ...chatItem.lxmf_message };
|
this.rawMessageData = { ...chatItem.lxmf_message };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export default class ToneGenerator {
|
|||||||
this.oscillator = null;
|
this.oscillator = null;
|
||||||
this.gainNode = null;
|
this.gainNode = null;
|
||||||
this.timeoutId = null;
|
this.timeoutId = null;
|
||||||
|
this.currentTone = null; // 'ringback', 'busy', or null
|
||||||
}
|
}
|
||||||
|
|
||||||
_initAudioContext() {
|
_initAudioContext() {
|
||||||
@@ -13,8 +14,10 @@ export default class ToneGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
playRingback() {
|
playRingback() {
|
||||||
|
if (this.currentTone === "ringback") return;
|
||||||
this._initAudioContext();
|
this._initAudioContext();
|
||||||
this.stop();
|
this.stop();
|
||||||
|
this.currentTone = "ringback";
|
||||||
|
|
||||||
const play = () => {
|
const play = () => {
|
||||||
const osc1 = this.audioCtx.createOscillator();
|
const osc1 = this.audioCtx.createOscillator();
|
||||||
@@ -57,8 +60,10 @@ export default class ToneGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
playBusyTone() {
|
playBusyTone() {
|
||||||
|
if (this.currentTone === "busy") return;
|
||||||
this._initAudioContext();
|
this._initAudioContext();
|
||||||
this.stop();
|
this.stop();
|
||||||
|
this.currentTone = "busy";
|
||||||
|
|
||||||
const play = () => {
|
const play = () => {
|
||||||
const osc = this.audioCtx.createOscillator();
|
const osc = this.audioCtx.createOscillator();
|
||||||
@@ -89,12 +94,13 @@ export default class ToneGenerator {
|
|||||||
};
|
};
|
||||||
|
|
||||||
play();
|
play();
|
||||||
|
|
||||||
// Auto-stop busy tone after 4 seconds (4 cycles)
|
// Auto-stop busy tone after 4 seconds (4 cycles)
|
||||||
setTimeout(() => this.stop(), 4000);
|
setTimeout(() => this.stop(), 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
this.currentTone = null;
|
||||||
if (this.timeoutId) {
|
if (this.timeoutId) {
|
||||||
clearTimeout(this.timeoutId);
|
clearTimeout(this.timeoutId);
|
||||||
this.timeoutId = null;
|
this.timeoutId = null;
|
||||||
@@ -102,21 +108,40 @@ export default class ToneGenerator {
|
|||||||
|
|
||||||
if (this.oscillator) {
|
if (this.oscillator) {
|
||||||
if (Array.isArray(this.oscillator)) {
|
if (Array.isArray(this.oscillator)) {
|
||||||
this.oscillator.forEach(osc => {
|
this.oscillator.forEach((osc) => {
|
||||||
try { osc.stop(); } catch (e) {}
|
try {
|
||||||
try { osc.disconnect(); } catch (e) {}
|
osc.stop();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors if oscillator is already stopped or disconnected
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
osc.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors if oscillator is already disconnected
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try { this.oscillator.stop(); } catch (e) {}
|
try {
|
||||||
try { this.oscillator.disconnect(); } catch (e) {}
|
this.oscillator.stop();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors if oscillator is already stopped or disconnected
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.oscillator.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors if oscillator is already disconnected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.oscillator = null;
|
this.oscillator = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.gainNode) {
|
if (this.gainNode) {
|
||||||
try { this.gainNode.disconnect(); } catch (e) {}
|
try {
|
||||||
|
this.gainNode.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors if gain node is already disconnected
|
||||||
|
}
|
||||||
this.gainNode = null;
|
this.gainNode = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def docs_manager(temp_dirs):
|
|||||||
|
|
||||||
def test_docs_manager_initialization(docs_manager, temp_dirs):
|
def test_docs_manager_initialization(docs_manager, temp_dirs):
|
||||||
_, docs_dir = temp_dirs
|
_, docs_dir = temp_dirs
|
||||||
assert docs_manager.docs_dir == docs_dir
|
assert docs_manager.docs_dir == os.path.join(docs_dir, "current")
|
||||||
assert os.path.exists(docs_dir)
|
assert os.path.exists(docs_dir)
|
||||||
assert docs_manager.download_status == "idle"
|
assert docs_manager.download_status == "idle"
|
||||||
|
|
||||||
@@ -44,9 +44,10 @@ def test_docs_manager_storage_dir_fallback(tmp_path):
|
|||||||
# If storage_dir is provided, it should be used for docs
|
# If storage_dir is provided, it should be used for docs
|
||||||
dm = DocsManager(config, str(public_dir), storage_dir=str(storage_dir))
|
dm = DocsManager(config, str(public_dir), storage_dir=str(storage_dir))
|
||||||
|
|
||||||
assert dm.docs_dir == os.path.join(str(storage_dir), "reticulum-docs")
|
assert dm.docs_dir == os.path.join(str(storage_dir), "reticulum-docs", "current")
|
||||||
assert dm.meshchatx_docs_dir == os.path.join(str(storage_dir), "meshchatx-docs")
|
assert dm.meshchatx_docs_dir == os.path.join(str(storage_dir), "meshchatx-docs")
|
||||||
assert os.path.exists(dm.docs_dir)
|
# The 'current' directory may not exist if there are no versions, but the base dir should exist
|
||||||
|
assert os.path.exists(dm.docs_base_dir)
|
||||||
assert os.path.exists(dm.meshchatx_docs_dir)
|
assert os.path.exists(dm.meshchatx_docs_dir)
|
||||||
|
|
||||||
|
|
||||||
@@ -77,7 +78,9 @@ def test_has_docs(docs_manager, temp_dirs):
|
|||||||
_, docs_dir = temp_dirs
|
_, docs_dir = temp_dirs
|
||||||
assert docs_manager.has_docs() is False
|
assert docs_manager.has_docs() is False
|
||||||
|
|
||||||
index_path = os.path.join(docs_dir, "index.html")
|
current_dir = os.path.join(docs_dir, "current")
|
||||||
|
os.makedirs(current_dir, exist_ok=True)
|
||||||
|
index_path = os.path.join(current_dir, "index.html")
|
||||||
with open(index_path, "w") as f:
|
with open(index_path, "w") as f:
|
||||||
f.write("<html></html>")
|
f.write("<html></html>")
|
||||||
|
|
||||||
@@ -108,7 +111,9 @@ def test_download_task_success(mock_get, docs_manager, temp_dirs):
|
|||||||
assert docs_manager.download_status == "completed"
|
assert docs_manager.download_status == "completed"
|
||||||
assert mock_extract.called
|
assert mock_extract.called
|
||||||
zip_path = os.path.join(docs_dir, "website.zip")
|
zip_path = os.path.join(docs_dir, "website.zip")
|
||||||
mock_extract.assert_called_with(zip_path)
|
call_args = mock_extract.call_args
|
||||||
|
assert call_args[0][0] == zip_path
|
||||||
|
assert call_args[0][1].startswith("git-")
|
||||||
|
|
||||||
|
|
||||||
@patch("requests.get")
|
@patch("requests.get")
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ def test_missed_call_notification(mock_app):
|
|||||||
mock_app.telephone_manager.call_is_incoming = True
|
mock_app.telephone_manager.call_is_incoming = True
|
||||||
mock_app.telephone_manager.call_status_at_end = 4 # Ringing
|
mock_app.telephone_manager.call_status_at_end = 4 # Ringing
|
||||||
mock_app.telephone_manager.call_start_time = time.time() - 10
|
mock_app.telephone_manager.call_start_time = time.time() - 10
|
||||||
|
mock_app.telephone_manager.call_was_established = False
|
||||||
|
|
||||||
mock_app.on_telephone_call_ended(caller_identity)
|
mock_app.on_telephone_call_ended(caller_identity)
|
||||||
|
|
||||||
@@ -291,8 +292,9 @@ def test_voicemail_notification_fuzzing(mock_app, remote_hash, remote_name, dura
|
|||||||
@given(
|
@given(
|
||||||
remote_hash=st.text(min_size=32, max_size=64), # Hex hash
|
remote_hash=st.text(min_size=32, max_size=64), # Hex hash
|
||||||
status_code=st.integers(min_value=0, max_value=10),
|
status_code=st.integers(min_value=0, max_value=10),
|
||||||
|
call_was_established=st.booleans(),
|
||||||
)
|
)
|
||||||
def test_missed_call_notification_fuzzing(mock_app, remote_hash, status_code):
|
def test_missed_call_notification_fuzzing(mock_app, remote_hash, status_code, call_was_established):
|
||||||
"""Fuzz missed call notification triggering."""
|
"""Fuzz missed call notification triggering."""
|
||||||
mock_app.database.misc.provider.execute("DELETE FROM notifications")
|
mock_app.database.misc.provider.execute("DELETE FROM notifications")
|
||||||
|
|
||||||
@@ -305,11 +307,13 @@ def test_missed_call_notification_fuzzing(mock_app, remote_hash, status_code):
|
|||||||
mock_app.telephone_manager.call_is_incoming = True
|
mock_app.telephone_manager.call_is_incoming = True
|
||||||
mock_app.telephone_manager.call_status_at_end = status_code
|
mock_app.telephone_manager.call_status_at_end = status_code
|
||||||
mock_app.telephone_manager.call_start_time = time.time()
|
mock_app.telephone_manager.call_start_time = time.time()
|
||||||
|
mock_app.telephone_manager.call_was_established = call_was_established
|
||||||
|
|
||||||
mock_app.on_telephone_call_ended(caller_identity)
|
mock_app.on_telephone_call_ended(caller_identity)
|
||||||
|
|
||||||
notifications = mock_app.database.misc.get_notifications()
|
notifications = mock_app.database.misc.get_notifications()
|
||||||
if status_code == 4: # Ringing
|
# Notification is created if incoming and not established, regardless of status_code
|
||||||
|
if not call_was_established:
|
||||||
assert len(notifications) == 1
|
assert len(notifications) == 1
|
||||||
assert notifications[0]["type"] == "telephone_missed_call"
|
assert notifications[0]["type"] == "telephone_missed_call"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1591,13 +1591,15 @@ def test_lxst_call_initiation_fuzzing(mock_app, destination_hash, timeout):
|
|||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
dest_hash_bytes = (
|
if isinstance(destination_hash, str) and len(destination_hash) == 32:
|
||||||
bytes.fromhex(destination_hash)
|
try:
|
||||||
if isinstance(destination_hash, str) and len(destination_hash) == 32
|
dest_hash_bytes = bytes.fromhex(destination_hash)
|
||||||
else destination_hash
|
except ValueError:
|
||||||
if isinstance(destination_hash, bytes)
|
dest_hash_bytes = os.urandom(16)
|
||||||
else os.urandom(16)
|
elif isinstance(destination_hash, bytes):
|
||||||
)
|
dest_hash_bytes = destination_hash
|
||||||
|
else:
|
||||||
|
dest_hash_bytes = os.urandom(16)
|
||||||
timeout_int = (
|
timeout_int = (
|
||||||
int(timeout)
|
int(timeout)
|
||||||
if isinstance(timeout, (int, float))
|
if isinstance(timeout, (int, float))
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import RNS
|
|
||||||
from meshchatx.src.backend.telephone_manager import TelephoneManager
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def telephone_manager():
|
|
||||||
identity = MagicMock(spec=RNS.Identity)
|
|
||||||
config_manager = MagicMock()
|
|
||||||
tm = TelephoneManager(identity, config_manager=config_manager)
|
|
||||||
tm.telephone = MagicMock()
|
|
||||||
tm.telephone.busy = False
|
|
||||||
return tm
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_initiation_status_updates(telephone_manager):
|
|
||||||
statuses = []
|
|
||||||
|
|
||||||
def status_callback(status, target_hash):
|
|
||||||
statuses.append((status, target_hash))
|
|
||||||
|
|
||||||
telephone_manager.on_initiation_status_callback = status_callback
|
|
||||||
destination_hash = b"\x01" * 32
|
|
||||||
|
|
||||||
# Mock RNS.Identity.recall to return an identity immediately
|
|
||||||
with patch.object(RNS.Identity, "recall") as mock_recall:
|
|
||||||
mock_identity = MagicMock(spec=RNS.Identity)
|
|
||||||
mock_recall.return_value = mock_identity
|
|
||||||
|
|
||||||
# Mock Transport to avoid Reticulum internal errors
|
|
||||||
with patch.object(RNS.Transport, "has_path", return_value=True):
|
|
||||||
with patch.object(RNS.Transport, "request_path"):
|
|
||||||
# Mock asyncio.to_thread to return immediately
|
|
||||||
with patch("asyncio.to_thread", return_value=None):
|
|
||||||
await telephone_manager.initiate(destination_hash)
|
|
||||||
|
|
||||||
# Check statuses: Resolving -> Dialing -> None
|
|
||||||
# Filter out None updates at the end for verification if they happen multiple times
|
|
||||||
final_statuses = [s[0] for s in statuses if s[0] is not None]
|
|
||||||
assert "Resolving identity..." in final_statuses
|
|
||||||
assert "Dialing..." in final_statuses
|
|
||||||
|
|
||||||
# Check that it cleared at the end
|
|
||||||
assert telephone_manager.initiation_status is None
|
|
||||||
assert statuses[-1] == (None, None)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_initiation_path_discovery_status(telephone_manager):
|
|
||||||
statuses = []
|
|
||||||
|
|
||||||
def status_callback(status, target_hash):
|
|
||||||
statuses.append((status, target_hash))
|
|
||||||
|
|
||||||
telephone_manager.on_initiation_status_callback = status_callback
|
|
||||||
destination_hash = b"\x02" * 32
|
|
||||||
|
|
||||||
# Mock RNS.Identity.recall to return None first, then an identity
|
|
||||||
with patch.object(RNS.Identity, "recall") as mock_recall:
|
|
||||||
mock_identity = MagicMock(spec=RNS.Identity)
|
|
||||||
mock_recall.side_effect = [None, None, mock_identity]
|
|
||||||
|
|
||||||
with patch.object(RNS.Transport, "has_path", return_value=False):
|
|
||||||
with patch.object(RNS.Transport, "request_path") as mock_request_path:
|
|
||||||
with patch("asyncio.to_thread", return_value=None):
|
|
||||||
# We need to speed up the sleep in initiate
|
|
||||||
with patch("asyncio.sleep", return_value=None):
|
|
||||||
await telephone_manager.initiate(destination_hash)
|
|
||||||
|
|
||||||
mock_request_path.assert_called_with(destination_hash)
|
|
||||||
|
|
||||||
final_statuses = [s[0] for s in statuses if s[0] is not None]
|
|
||||||
assert "Resolving identity..." in final_statuses
|
|
||||||
assert "Discovering path/identity..." in final_statuses
|
|
||||||
assert "Dialing..." in final_statuses
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_initiation_failure_status(telephone_manager):
|
|
||||||
statuses = []
|
|
||||||
|
|
||||||
def status_callback(status, target_hash):
|
|
||||||
statuses.append((status, target_hash))
|
|
||||||
|
|
||||||
telephone_manager.on_initiation_status_callback = status_callback
|
|
||||||
destination_hash = b"\x03" * 32
|
|
||||||
|
|
||||||
# Mock failure
|
|
||||||
with patch.object(RNS.Identity, "recall", side_effect=RuntimeError("Test Error")):
|
|
||||||
with patch("asyncio.sleep", return_value=None):
|
|
||||||
with pytest.raises(RuntimeError, match="Test Error"):
|
|
||||||
await telephone_manager.initiate(destination_hash)
|
|
||||||
|
|
||||||
# Should have a failure status
|
|
||||||
failure_statuses = [s[0] for s in statuses if s[0] and s[0].startswith("Failed:")]
|
|
||||||
assert len(failure_statuses) > 0
|
|
||||||
assert "Failed: Test Error" in failure_statuses[0]
|
|
||||||
|
|
||||||
# Should still clear at the end
|
|
||||||
assert telephone_manager.initiation_status is None
|
|
||||||
@@ -72,17 +72,22 @@ def test_generate_greeting(mock_deps, temp_dir):
|
|||||||
def test_start_recording_currently_disabled(mock_deps, temp_dir):
|
def test_start_recording_currently_disabled(mock_deps, temp_dir):
|
||||||
mock_db = MagicMock()
|
mock_db = MagicMock()
|
||||||
mock_config = MagicMock()
|
mock_config = MagicMock()
|
||||||
|
mock_tel_manager = MagicMock()
|
||||||
mock_tel = MagicMock()
|
mock_tel = MagicMock()
|
||||||
vm = VoicemailManager(mock_db, mock_config, mock_tel, temp_dir)
|
mock_tel_manager.telephone = mock_tel
|
||||||
|
vm = VoicemailManager(mock_db, mock_config, mock_tel_manager, temp_dir)
|
||||||
|
|
||||||
mock_link = MagicMock()
|
mock_caller_identity = MagicMock()
|
||||||
mock_remote_id = MagicMock()
|
# Use actual bytes for hash, which has a .hex() method
|
||||||
mock_remote_id.hash = b"remote_hash"
|
mock_caller_identity.hash = b"remote_hash_32_bytes_long_012345"
|
||||||
mock_link.get_remote_identity.return_value = mock_remote_id
|
|
||||||
|
|
||||||
vm.start_recording(mock_link)
|
# Recording requires telephone.active_call to exist
|
||||||
|
# Without it, recording won't start
|
||||||
|
mock_tel.active_call = None
|
||||||
|
|
||||||
# It's currently disabled in code, so it should stay False
|
vm.start_recording(mock_caller_identity)
|
||||||
|
|
||||||
|
# Without active_call, recording should not start
|
||||||
assert vm.is_recording is False
|
assert vm.is_recording is False
|
||||||
|
|
||||||
|
|
||||||
@@ -95,6 +100,8 @@ def test_stop_recording(mock_deps, temp_dir):
|
|||||||
vm.is_recording = True
|
vm.is_recording = True
|
||||||
mock_pipeline_inst = MagicMock()
|
mock_pipeline_inst = MagicMock()
|
||||||
vm.recording_pipeline = mock_pipeline_inst
|
vm.recording_pipeline = mock_pipeline_inst
|
||||||
|
mock_sink = MagicMock()
|
||||||
|
vm.recording_sink = mock_sink
|
||||||
vm.recording_filename = "test.opus"
|
vm.recording_filename = "test.opus"
|
||||||
|
|
||||||
mock_remote_id = MagicMock()
|
mock_remote_id = MagicMock()
|
||||||
@@ -107,11 +114,20 @@ def test_stop_recording(mock_deps, temp_dir):
|
|||||||
|
|
||||||
vm.get_name_for_identity_hash = MagicMock(return_value="Test User")
|
vm.get_name_for_identity_hash = MagicMock(return_value="Test User")
|
||||||
|
|
||||||
with patch("time.time", return_value=110):
|
# Create the recording file so add_voicemail gets called
|
||||||
|
recording_path = os.path.join(vm.recordings_dir, "test.opus")
|
||||||
|
os.makedirs(vm.recordings_dir, exist_ok=True)
|
||||||
|
with open(recording_path, "wb") as f:
|
||||||
|
f.write(b"fake opus data")
|
||||||
|
|
||||||
|
with patch("time.time", return_value=110), patch.object(
|
||||||
|
vm, "_fix_recording"
|
||||||
|
) as mock_fix:
|
||||||
vm.stop_recording()
|
vm.stop_recording()
|
||||||
|
|
||||||
assert vm.is_recording is False
|
assert vm.is_recording is False
|
||||||
mock_pipeline_inst.stop.assert_called()
|
mock_pipeline_inst.stop.assert_called()
|
||||||
|
mock_sink.stop.assert_called()
|
||||||
mock_db.voicemails.add_voicemail.assert_called()
|
mock_db.voicemails.add_voicemail.assert_called()
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +143,8 @@ def test_start_voicemail_session(mock_deps, temp_dir):
|
|||||||
mock_caller = MagicMock()
|
mock_caller = MagicMock()
|
||||||
mock_caller.hash = b"caller"
|
mock_caller.hash = b"caller"
|
||||||
|
|
||||||
|
# Set call_status to RINGING (4) so answer() gets called
|
||||||
|
mock_tel.call_status = 4
|
||||||
mock_tel.answer.return_value = True
|
mock_tel.answer.return_value = True
|
||||||
mock_tel.audio_input = MagicMock()
|
mock_tel.audio_input = MagicMock()
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class MockAudioContext {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.state = "suspended";
|
this.state = "suspended";
|
||||||
this.currentTime = 0;
|
this.currentTime = 0;
|
||||||
|
this.destination = {};
|
||||||
}
|
}
|
||||||
decodeAudioData() {
|
decodeAudioData() {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
@@ -37,6 +38,7 @@ class MockAudioContext {
|
|||||||
// Mock fetch
|
// Mock fetch
|
||||||
global.fetch = vi.fn(() =>
|
global.fetch = vi.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user