From cadf6fe66f5689cd228d2eda6be81e452a6d68ff Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Thu, 1 Jan 2026 17:34:47 -0600 Subject: [PATCH] feat(voicemail): refactor VoicemailManager to use config for settings, add greeting conversion and removal methods --- meshchatx/src/backend/voicemail_manager.py | 92 +++++++++++++++------- 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/meshchatx/src/backend/voicemail_manager.py b/meshchatx/src/backend/voicemail_manager.py index b8911b9..ecf12c3 100644 --- a/meshchatx/src/backend/voicemail_manager.py +++ b/meshchatx/src/backend/voicemail_manager.py @@ -14,8 +14,9 @@ from LXST.Sources import OpusFileSource class VoicemailManager: - def __init__(self, db, telephone_manager, storage_dir): + def __init__(self, db, config, telephone_manager, storage_dir): self.db = db + self.config = config self.telephone_manager = telephone_manager self.storage_dir = os.path.join(storage_dir, "voicemails") self.greetings_dir = os.path.join(self.storage_dir, "greetings") @@ -50,6 +51,10 @@ class VoicemailManager: else: RNS.log("Voicemail: ffmpeg not found", RNS.LOG_ERROR) + def get_name_for_identity_hash(self, identity_hash): + """Default implementation, should be patched by ReticulumMeshChat""" + return None + def _find_espeak(self): # Try standard name first path = shutil.which("espeak-ng") @@ -103,53 +108,82 @@ class VoicemailManager: # espeak-ng to WAV subprocess.run([self.espeak_path, "-w", wav_path, text], check=True) - # ffmpeg to Opus - if os.path.exists(opus_path): - os.remove(opus_path) - - subprocess.run( - [ - self.ffmpeg_path, - "-i", - wav_path, - "-c:a", - "libopus", - "-b:a", - "16k", - "-vbr", - "on", - opus_path, - ], - check=True, - ) - - return opus_path + # Convert WAV to Opus + return self.convert_to_greeting(wav_path) finally: if os.path.exists(wav_path): os.remove(wav_path) + def convert_to_greeting(self, input_path): + if not self.has_ffmpeg: + msg = "ffmpeg is required for audio conversion" + raise RuntimeError(msg) + + opus_path = os.path.join(self.greetings_dir, "greeting.opus") + + if os.path.exists(opus_path): + os.remove(opus_path) + + subprocess.run( + [ + self.ffmpeg_path, + "-i", + input_path, + "-c:a", + "libopus", + "-b:a", + "16k", + "-vbr", + "on", + opus_path, + ], + check=True, + ) + + return opus_path + + def remove_greeting(self): + opus_path = os.path.join(self.greetings_dir, "greeting.opus") + if os.path.exists(opus_path): + os.remove(opus_path) + return True + def handle_incoming_call(self, caller_identity): - if not self.db.config.voicemail_enabled.get(): + RNS.log(f"Voicemail: handle_incoming_call from {RNS.prettyhexrep(caller_identity.hash)}", RNS.LOG_DEBUG) + if not self.config.voicemail_enabled.get(): + RNS.log("Voicemail: Voicemail is disabled", RNS.LOG_DEBUG) return - delay = self.db.config.voicemail_auto_answer_delay_seconds.get() + delay = self.config.voicemail_auto_answer_delay_seconds.get() + RNS.log(f"Voicemail: Will auto-answer in {delay} seconds", RNS.LOG_DEBUG) def voicemail_job(): + RNS.log(f"Voicemail: Auto-answer timer started for {RNS.prettyhexrep(caller_identity.hash)}", RNS.LOG_DEBUG) time.sleep(delay) # Check if still ringing and no other active call telephone = self.telephone_manager.telephone + if not telephone: + RNS.log("Voicemail: No telephone object", RNS.LOG_ERROR) + return + + RNS.log(f"Voicemail: Checking status. Call status: {telephone.call_status}, Active call: {telephone.active_call}", RNS.LOG_DEBUG) + if ( telephone and telephone.active_call - and telephone.active_call.get_remote_identity() == caller_identity + and telephone.active_call.get_remote_identity().hash == caller_identity.hash and telephone.call_status == LXST.Signalling.STATUS_RINGING ): RNS.log( f"Auto-answering call from {RNS.prettyhexrep(caller_identity.hash)} for voicemail", - RNS.LOG_DEBUG, + RNS.LOG_INFO, ) self.start_voicemail_session(caller_identity) + else: + RNS.log("Voicemail: Auto-answer conditions not met after delay", RNS.LOG_DEBUG) + if telephone.active_call: + RNS.log(f"Voicemail: Active call remote: {RNS.prettyhexrep(telephone.active_call.get_remote_identity().hash)}", RNS.LOG_DEBUG) threading.Thread(target=voicemail_job, daemon=True).start() @@ -170,7 +204,7 @@ class VoicemailManager: greeting_path = os.path.join(self.greetings_dir, "greeting.opus") if not os.path.exists(greeting_path): # Fallback if no greeting generated yet - self.generate_greeting(self.db.config.voicemail_greeting.get()) + self.generate_greeting(self.config.voicemail_greeting.get()) def session_job(): try: @@ -206,7 +240,7 @@ class VoicemailManager: self.start_recording(caller_identity) # 4. Wait for max recording time or hangup - max_time = self.db.config.voicemail_max_recording_seconds.get() + max_time = self.config.voicemail_max_recording_seconds.get() start_wait = time.time() while self.is_recording and (time.time() - start_wait < max_time): time.sleep(0.5) @@ -271,7 +305,7 @@ class VoicemailManager: # Save to database if long enough if duration >= 1: - remote_name = self.telephone_manager.get_name_for_identity_hash( + remote_name = self.get_name_for_identity_hash( self.recording_remote_identity.hash.hex() ) self.db.voicemails.add_voicemail(