mirror of
https://github.com/liamcottle/reticulum-meshchat.git
synced 2026-04-27 16:10:32 +00:00
fix(datetime): handle Peewee DateTimeField type confusion and JSON serialization
Peewee's DateTimeField returns Python datetime objects, not strings, but meshchat.py called datetime.strptime() and datetime.fromisoformat() on them — causing TypeError crashes. Additionally, raw datetime objects passed to web.json_response() caused JSON serialization failures since json.dumps() cannot serialize datetime. Changes: - Add _ensure_datetime() helper that accepts both str and datetime - Add _datetime_to_str() helper for safe JSON serialization - Fix fromisoformat() calls in signal metrics endpoint - Fix strptime() calls in conversation read state check - Wrap all created_at/updated_at fields in converter dicts - Fix parent_interface_hash bytes→hex guard in interface stats Fixes #144, #146. Related to #139. https://claude.ai/code/session_01A3nv8XN4z6yyp8tHNGAS8M
This commit is contained in:
parent
011876bec5
commit
2f519d9efe
1 changed files with 40 additions and 15 deletions
55
meshchat.py
55
meshchat.py
|
|
@ -34,6 +34,27 @@ from src.backend.audio_call_manager import AudioCall, AudioCallManager
|
|||
from src.backend.sideband_commands import SidebandCommands
|
||||
|
||||
|
||||
# helper to safely convert peewee DateTimeField values (may be datetime or str depending on version)
|
||||
def _ensure_datetime(value):
|
||||
"""Convert string to datetime if needed; pass through datetime objects."""
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S.%f%z")
|
||||
raise TypeError(f"Expected datetime or str, got {type(value)}")
|
||||
|
||||
|
||||
# helper to safely convert datetime to ISO 8601 string for JSON serialization
|
||||
def _datetime_to_str(value):
|
||||
"""Convert datetime to ISO 8601 string for JSON serialization."""
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
return value # already a string
|
||||
|
||||
|
||||
# NOTE: this is required to be able to pack our app with cxfreeze as an exe, otherwise it can't access bundled assets
|
||||
# this returns a file path based on if we are running meshchat.py directly, or if we have packed it as an exe with cxfreeze
|
||||
# https://cx-freeze.readthedocs.io/en/latest/faq.html#using-data-files
|
||||
|
|
@ -1420,8 +1441,8 @@ class ReticulumMeshChat:
|
|||
"operator_display_name": operator_display_name,
|
||||
"is_propagation_enabled": is_propagation_enabled,
|
||||
"per_transfer_limit": per_transfer_limit,
|
||||
"created_at": announce.created_at,
|
||||
"updated_at": announce.updated_at,
|
||||
"created_at": _datetime_to_str(announce.created_at),
|
||||
"updated_at": _datetime_to_str(announce.updated_at),
|
||||
})
|
||||
|
||||
return web.json_response({
|
||||
|
|
@ -1527,12 +1548,12 @@ class ReticulumMeshChat:
|
|||
# determine when latest announce was received
|
||||
latest_announce_at = None
|
||||
if latest_announce is not None:
|
||||
latest_announce_at = datetime.fromisoformat(latest_announce.updated_at)
|
||||
latest_announce_at = _ensure_datetime(latest_announce.updated_at)
|
||||
|
||||
# determine when latest lxmf message was received
|
||||
latest_lxmf_message_at = None
|
||||
if latest_lxmf_message is not None:
|
||||
latest_lxmf_message_at = datetime.fromisoformat(latest_lxmf_message.created_at)
|
||||
latest_lxmf_message_at = _ensure_datetime(latest_lxmf_message.created_at)
|
||||
|
||||
# get signal metrics from latest announce
|
||||
if latest_announce is not None:
|
||||
|
|
@ -1555,7 +1576,7 @@ class ReticulumMeshChat:
|
|||
"snr": snr,
|
||||
"rssi": rssi,
|
||||
"quality": quality,
|
||||
"updated_at": updated_at,
|
||||
"updated_at": _datetime_to_str(updated_at),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -1728,6 +1749,10 @@ class ReticulumMeshChat:
|
|||
# ensure probe_responder is hex as json_response can't serialize bytes
|
||||
if "probe_responder" in interface_stats and interface_stats["probe_responder"] is not None:
|
||||
interface_stats["probe_responder"] = interface_stats["probe_responder"].hex()
|
||||
|
||||
# ensure network_id is hex as json_response can't serialize bytes
|
||||
if "network_id" in interface_stats and interface_stats["network_id"] is not None:
|
||||
interface_stats["network_id"] = interface_stats["network_id"].hex()
|
||||
|
||||
# ensure ifac_signature is hex as json_response can't serialize bytes
|
||||
for interface in interface_stats["interfaces"]:
|
||||
|
|
@ -1735,7 +1760,7 @@ class ReticulumMeshChat:
|
|||
if "short_name" in interface:
|
||||
interface["interface_name"] = interface["short_name"]
|
||||
|
||||
if "parent_interface_name" in interface and interface["parent_interface_name"] is not None:
|
||||
if "parent_interface_hash" in interface and isinstance(interface["parent_interface_hash"], bytes):
|
||||
interface["parent_interface_hash"] = interface["parent_interface_hash"].hex()
|
||||
|
||||
if "ifac_signature" in interface and interface["ifac_signature"]:
|
||||
|
|
@ -2016,7 +2041,7 @@ class ReticulumMeshChat:
|
|||
"lxmf_user_icon": lxmf_user_icon,
|
||||
# we say the conversation was updated when the latest message was created
|
||||
# otherwise this will go crazy when sending a message, as the updated_at on the latest message changes very frequently
|
||||
"updated_at": created_at,
|
||||
"updated_at": _datetime_to_str(created_at),
|
||||
})
|
||||
|
||||
return web.json_response({
|
||||
|
|
@ -2622,8 +2647,8 @@ class ReticulumMeshChat:
|
|||
"display_name": display_name,
|
||||
"custom_display_name": self.get_custom_destination_display_name(announce.destination_hash),
|
||||
"lxmf_user_icon": lxmf_user_icon,
|
||||
"created_at": announce.created_at,
|
||||
"updated_at": announce.updated_at,
|
||||
"created_at": _datetime_to_str(announce.created_at),
|
||||
"updated_at": _datetime_to_str(announce.updated_at),
|
||||
}
|
||||
|
||||
# convert database favourite to a dictionary
|
||||
|
|
@ -2633,8 +2658,8 @@ class ReticulumMeshChat:
|
|||
"destination_hash": favourite.destination_hash,
|
||||
"display_name": favourite.display_name,
|
||||
"aspect": favourite.aspect,
|
||||
"created_at": favourite.created_at,
|
||||
"updated_at": favourite.updated_at,
|
||||
"created_at": _datetime_to_str(favourite.created_at),
|
||||
"updated_at": _datetime_to_str(favourite.updated_at),
|
||||
}
|
||||
|
||||
# convert database lxmf message to a dictionary
|
||||
|
|
@ -2658,8 +2683,8 @@ class ReticulumMeshChat:
|
|||
"rssi": db_lxmf_message.rssi,
|
||||
"snr": db_lxmf_message.snr,
|
||||
"quality": db_lxmf_message.quality,
|
||||
"created_at": db_lxmf_message.created_at,
|
||||
"updated_at": db_lxmf_message.updated_at,
|
||||
"created_at": _datetime_to_str(db_lxmf_message.created_at),
|
||||
"updated_at": _datetime_to_str(db_lxmf_message.updated_at),
|
||||
}
|
||||
|
||||
# updates the lxmf user icon for the provided destination hash
|
||||
|
|
@ -3278,8 +3303,8 @@ class ReticulumMeshChat:
|
|||
return True
|
||||
|
||||
# conversation is unread if last read at is before the latest incoming message creation date
|
||||
conversation_last_read_at = datetime.strptime(lxmf_conversation_read_state.last_read_at, "%Y-%m-%d %H:%M:%S.%f%z")
|
||||
conversation_latest_message_at = datetime.strptime(latest_incoming_lxmf_message.created_at, "%Y-%m-%d %H:%M:%S.%f%z")
|
||||
conversation_last_read_at = _ensure_datetime(lxmf_conversation_read_state.last_read_at)
|
||||
conversation_latest_message_at = _ensure_datetime(latest_incoming_lxmf_message.created_at)
|
||||
return conversation_last_read_at < conversation_latest_message_at
|
||||
|
||||
# returns number of messages that failed to send in a conversation
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue