From a6b01321a36e8e6543ea567f479818d1fe127a2d Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Thu, 1 Jan 2026 21:08:17 -0600 Subject: [PATCH] feat(voicemail): implement new voicemail notification callback and enhance call history retrieval with search and pagination support --- meshchatx/meshchat.py | 55 ++++++++++++++++++++- meshchatx/src/backend/config_manager.py | 3 ++ meshchatx/src/backend/database/schema.py | 7 ++- meshchatx/src/backend/database/telephone.py | 15 ++++-- meshchatx/src/backend/voicemail_manager.py | 9 ++++ 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py index a6b9107..6957e20 100644 --- a/meshchatx/meshchat.py +++ b/meshchatx/meshchat.py @@ -372,6 +372,7 @@ class ReticulumMeshChat: self.voicemail_manager.get_name_for_identity_hash = ( self.get_name_for_identity_hash ) + self.voicemail_manager.on_new_voicemail_callback = self.on_new_voicemail_received # init Ringtone Manager self.ringtone_manager = RingtoneManager( @@ -1335,8 +1336,39 @@ class ReticulumMeshChat: value = value.lower() return value in {"1", "true", "yes", "on"} + def on_new_voicemail_received(self, remote_hash, remote_name, duration): + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "new_voicemail", + "remote_identity_hash": remote_hash, + "remote_identity_name": remote_name, + "duration": duration, + "timestamp": time.time(), + }, + ), + ), + ) + # handle receiving a new audio call def on_incoming_telephone_call(self, caller_identity: RNS.Identity): + caller_hash = caller_identity.hash.hex() + + # Check if caller is blocked + if self.is_destination_blocked(caller_hash): + print(f"Rejecting incoming call from blocked source: {caller_hash}") + if self.telephone_manager.telephone: + self.telephone_manager.telephone.hangup() + return + + # Check for Do Not Disturb + if self.config.do_not_disturb_enabled.get(): + print(f"Rejecting incoming call due to Do Not Disturb: {caller_hash}") + if self.telephone_manager.telephone: + self.telephone_manager.telephone.hangup() + return + # Trigger voicemail handling self.voicemail_manager.handle_incoming_call(caller_identity) @@ -1403,6 +1435,21 @@ class ReticulumMeshChat: timestamp=time.time(), ) + # Trigger missed call notification if it was an incoming call that ended while ringing + if is_incoming and status_code == 4: + AsyncUtils.run_async( + self.websocket_broadcast( + json.dumps( + { + "type": "telephone_missed_call", + "remote_identity_hash": remote_identity_hash, + "remote_identity_name": remote_identity_name, + "timestamp": time.time(), + }, + ), + ), + ) + AsyncUtils.run_async( self.websocket_broadcast( json.dumps( @@ -3139,7 +3186,13 @@ class ReticulumMeshChat: @routes.get("/api/v1/telephone/history") async def telephone_history(request): limit = int(request.query.get("limit", 10)) - history = self.database.telephone.get_call_history(limit=limit) + offset = int(request.query.get("offset", 0)) + search = request.query.get("search", None) + history = self.database.telephone.get_call_history( + search=search, + limit=limit, + offset=offset, + ) call_history = [] for row in history: diff --git a/meshchatx/src/backend/config_manager.py b/meshchatx/src/backend/config_manager.py index b4ef009..67e5e81 100644 --- a/meshchatx/src/backend/config_manager.py +++ b/meshchatx/src/backend/config_manager.py @@ -135,6 +135,9 @@ class ConfigManager: self.custom_ringtone_enabled = self.BoolConfig(self, "custom_ringtone_enabled", False) self.ringtone_filename = self.StringConfig(self, "ringtone_filename", None) + # telephony config + self.do_not_disturb_enabled = self.BoolConfig(self, "do_not_disturb_enabled", False) + # map config self.map_offline_enabled = self.BoolConfig(self, "map_offline_enabled", False) self.map_offline_path = self.StringConfig(self, "map_offline_path", None) diff --git a/meshchatx/src/backend/database/schema.py b/meshchatx/src/backend/database/schema.py index 17fdbfc..0585251 100644 --- a/meshchatx/src/backend/database/schema.py +++ b/meshchatx/src/backend/database/schema.py @@ -2,7 +2,7 @@ from .provider import DatabaseProvider class DatabaseSchema: - LATEST_VERSION = 18 + LATEST_VERSION = 19 def __init__(self, provider: DatabaseProvider): self.provider = provider @@ -551,6 +551,11 @@ class DatabaseSchema: "CREATE INDEX IF NOT EXISTS idx_contacts_remote_identity_hash ON contacts(remote_identity_hash)", ) + if current_version < 19: + self.provider.execute( + "CREATE INDEX IF NOT EXISTS idx_call_history_remote_name ON call_history(remote_identity_name)", + ) + # Update version in config self.provider.execute( """ diff --git a/meshchatx/src/backend/database/telephone.py b/meshchatx/src/backend/database/telephone.py index eeb28cf..2fe2f14 100644 --- a/meshchatx/src/backend/database/telephone.py +++ b/meshchatx/src/backend/database/telephone.py @@ -40,10 +40,19 @@ class TelephoneDAO: ), ) - def get_call_history(self, limit=10): + def get_call_history(self, search=None, limit=10, offset=0): + if search: + return self.provider.fetchall( + """ + SELECT * FROM call_history + WHERE remote_identity_name LIKE ? OR remote_identity_hash LIKE ? + ORDER BY timestamp DESC LIMIT ? OFFSET ? + """, + (f"%{search}%", f"%{search}%", limit, offset), + ) return self.provider.fetchall( - "SELECT * FROM call_history ORDER BY timestamp DESC LIMIT ?", - (limit,), + "SELECT * FROM call_history ORDER BY timestamp DESC LIMIT ? OFFSET ?", + (limit, offset), ) def clear_call_history(self): diff --git a/meshchatx/src/backend/voicemail_manager.py b/meshchatx/src/backend/voicemail_manager.py index 5f8837e..cfd128b 100644 --- a/meshchatx/src/backend/voicemail_manager.py +++ b/meshchatx/src/backend/voicemail_manager.py @@ -34,6 +34,8 @@ class VoicemailManager: self.recording_remote_identity = None self.recording_filename = None + self.on_new_voicemail_callback = None + # Paths to executables self.espeak_path = self._find_espeak() self.ffmpeg_path = self._find_ffmpeg() @@ -377,6 +379,13 @@ class VoicemailManager: f"Saved voicemail from {RNS.prettyhexrep(self.recording_remote_identity.hash)} ({duration}s)", RNS.LOG_DEBUG, ) + + if self.on_new_voicemail_callback: + self.on_new_voicemail_callback( + self.recording_remote_identity.hash.hex(), + remote_name, + duration, + ) else: # Delete short/empty recording filepath = os.path.join(self.recordings_dir, self.recording_filename)