refactor(dm): Use native SENT_MSG from meshcore-cli 1.3.12+ instead of custom log

Remove workaround for meshcore-cli 1.3.11 bug where SENT_MSG contained
sender name instead of recipient. Now using native SENT_MSG entries from
.msgs file with correct recipient and sender fields (requires meshcore-cli >= 1.3.12).

Changes:
- Add _parse_sent_msg() to parse SENT_MSG from .msgs file
- Update read_dm_messages() to process both PRIV and SENT_MSG from .msgs
- Remove save_sent_dm(), _read_sent_dm_log(), _parse_sent_dm_entry()
- Remove dm_sent_log_path property from config
- Add _cleanup_old_dm_sent_log() to remove obsolete log file
- Update comments and documentation

Benefits:
- Single source of truth (.msgs file only)
- Simpler codebase (-95 lines)
- No custom workarounds
- Better data consistency

Related: meshcore-cli update to 1.3.12 (commit ad4a7b3)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2025-12-27 19:48:39 +01:00
parent ad4a7b3f49
commit 879f7041e7
3 changed files with 59 additions and 100 deletions

View File

@@ -42,11 +42,6 @@ class Config:
"""Get the full path to archive directory"""
return Path(self.MC_ARCHIVE_DIR)
@property
def dm_sent_log_path(self) -> Path:
"""Get the full path to our sent DM log file (workaround for meshcore-cli bug)"""
return Path(self.MC_CONFIG_DIR) / f"{self.MC_DEVICE_NAME}_dm_sent.jsonl"
def __repr__(self):
return (
f"Config(device={self.MC_DEVICE_NAME}, "

View File

@@ -314,71 +314,38 @@ def delete_channel_messages(channel_idx: int) -> bool:
# Direct Messages (DM) Parsing
# =============================================================================
#
# Note: meshcore-cli has a bug where SENT_MSG entries contain the sender's
# device name instead of the recipient's name. To work around this, we maintain
# our own sent DM log file with correct recipient information.
# See: https://github.com/liamcottle/meshcore-cli/issues/XXX
# Requires meshcore-cli >= 1.3.12 for correct SENT_MSG format with recipient field.
#
# Message types:
# - PRIV: Incoming private messages (from others to us)
# - SENT_MSG: Outgoing private messages (from us to others) - txt_type=0 for DM
# =============================================================================
def save_sent_dm(recipient: str, text: str) -> bool:
# Global flag to track if cleanup has been performed
_dm_cleanup_done = False
def _cleanup_old_dm_sent_log() -> None:
"""
Save a sent DM to our own log file (workaround for meshcore-cli bug).
Clean up the old _dm_sent.jsonl file that was used as a workaround
for meshcore-cli 1.3.11 bug. This file is no longer needed with 1.3.12+.
Args:
recipient: Contact name the message was sent to
text: Message content
Returns:
True if saved successfully, False otherwise
This function is called once at the first read_dm_messages() invocation.
"""
dm_log_file = config.dm_sent_log_path
global _dm_cleanup_done
entry = {
'timestamp': int(time.time()),
'recipient': recipient,
'text': text,
'status': 'pending'
}
if _dm_cleanup_done:
return
try:
with open(dm_log_file, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
logger.info(f"Saved sent DM to {recipient}")
return True
dm_log_file = Path(config.MC_CONFIG_DIR) / f"{config.MC_DEVICE_NAME}_dm_sent.jsonl"
if dm_log_file.exists():
dm_log_file.unlink()
logger.info(f"Cleaned up old DM sent log: {dm_log_file}")
except Exception as e:
logger.error(f"Error saving sent DM: {e}")
return False
def _read_sent_dm_log() -> List[Dict]:
"""
Read sent DMs from our own log file.
Returns:
List of sent DM entries
"""
dm_log_file = config.dm_sent_log_path
if not dm_log_file.exists():
return []
entries = []
try:
with open(dm_log_file, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
entries.append(data)
except json.JSONDecodeError as e:
logger.warning(f"Invalid JSON in DM log at line {line_num}: {e}")
continue
except Exception as e:
logger.error(f"Error reading sent DM log: {e}")
return entries
logger.warning(f"Could not clean up old DM sent log: {e}")
finally:
_dm_cleanup_done = True
def _parse_priv_message(line: Dict) -> Optional[Dict]:
@@ -428,22 +395,32 @@ def _parse_priv_message(line: Dict) -> Optional[Dict]:
}
def _parse_sent_dm_entry(entry: Dict) -> Optional[Dict]:
def _parse_sent_msg(line: Dict) -> Optional[Dict]:
"""
Parse a sent DM entry from our own log file.
Parse outgoing private message (SENT_MSG type) from meshcore-cli 1.3.12+.
This function parses SENT_MSG entries from the .msgs file. As of meshcore-cli 1.3.12,
these entries now correctly include both 'recipient' and 'sender' fields.
Args:
entry: Entry from our dm_sent.jsonl file
line: Raw JSON object from .msgs file with type='SENT_MSG'
Returns:
Parsed DM dict or None if invalid
Parsed DM dict or None if invalid or not a private message
"""
text = entry.get('text', '').strip()
text = line.get('text', '').strip()
if not text:
return None
timestamp = entry.get('timestamp', 0)
recipient = entry.get('recipient', 'Unknown')
# Check txt_type - only process private messages (0), not channel messages (1)
txt_type = line.get('txt_type', 0)
if txt_type != 0:
return None
timestamp = line.get('timestamp', 0)
# Use 'recipient' field (added in meshcore-cli 1.3.12), fallback to 'name'
recipient = line.get('recipient', line.get('name', 'Unknown'))
sender = line.get('sender', config.MC_DEVICE_NAME)
# Generate conversation ID from recipient name
conversation_id = f"name_{recipient}"
@@ -452,18 +429,16 @@ def _parse_sent_dm_entry(entry: Dict) -> Optional[Dict]:
text_hash = hash(text[:50]) & 0xFFFFFFFF
dedup_key = f"sent_{timestamp}_{text_hash}"
# Keep the status from log file (pending by default, no ACK tracking available)
status = entry.get('status', 'pending')
return {
'type': 'dm',
'direction': 'outgoing',
'recipient': recipient,
'sender': sender,
'content': text,
'timestamp': timestamp,
'datetime': datetime.fromtimestamp(timestamp).isoformat() if timestamp > 0 else None,
'is_own': True,
'status': status,
'txt_type': txt_type,
'conversation_id': conversation_id,
'dedup_key': dedup_key
}
@@ -475,10 +450,9 @@ def read_dm_messages(
days: Optional[int] = 7
) -> Tuple[List[Dict], Dict[str, str]]:
"""
Read and parse DM messages from .msgs file (incoming) and our sent DM log (outgoing).
Read and parse DM messages from .msgs file (both incoming PRIV and outgoing SENT_MSG).
Note: We ignore SENT_MSG entries from .msgs because they have the wrong recipient
due to a bug in meshcore-cli.
Requires meshcore-cli >= 1.3.12 for correct SENT_MSG format with recipient field.
Args:
limit: Maximum messages to return (None = all)
@@ -493,7 +467,10 @@ def read_dm_messages(
seen_dedup_keys = set()
pubkey_to_name = {} # Map pubkey_prefix -> most recent name
# --- Read incoming messages (PRIV) from .msgs file ---
# Clean up old DM sent log file (once per session)
_cleanup_old_dm_sent_log()
# --- Read DM messages from .msgs file ---
msgs_file = config.msgs_file_path
if msgs_file.exists():
try:
@@ -505,17 +482,21 @@ def read_dm_messages(
try:
data = json.loads(line)
msg_type = data.get('type')
# Only process PRIV messages (incoming DMs)
if data.get('type') != 'PRIV':
continue
# Process PRIV (incoming) and SENT_MSG (outgoing) messages
if msg_type == 'PRIV':
parsed = _parse_priv_message(data)
elif msg_type == 'SENT_MSG':
parsed = _parse_sent_msg(data)
else:
continue # Ignore other message types
parsed = _parse_priv_message(data)
if not parsed:
continue
# Update pubkey->name mapping
if parsed.get('pubkey_prefix'):
# Update pubkey->name mapping (only for PRIV messages)
if msg_type == 'PRIV' and parsed.get('pubkey_prefix'):
pubkey_to_name[parsed['pubkey_prefix']] = parsed['sender']
# Deduplicate
@@ -529,26 +510,12 @@ def read_dm_messages(
logger.warning(f"Invalid JSON at line {line_num}: {e}")
continue
except Exception as e:
logger.error(f"Error parsing DM at line {line_num}: {e}")
logger.error(f"Error parsing message at line {line_num}: {e}")
continue
except Exception as e:
logger.error(f"Error reading messages file: {e}")
# --- Read sent DMs from our own log file ---
sent_entries = _read_sent_dm_log()
for entry in sent_entries:
parsed = _parse_sent_dm_entry(entry)
if not parsed:
continue
# Deduplicate
if parsed['dedup_key'] in seen_dedup_keys:
continue
seen_dedup_keys.add(parsed['dedup_key'])
messages.append(parsed)
# --- Filter by conversation if specified ---
if conversation_id:
filtered_messages = []

View File

@@ -1069,9 +1069,6 @@ def send_dm_message():
success, message = cli.send_dm(recipient, text)
if success:
# Save to our own sent DM log (workaround for meshcore-cli bug)
parser.save_sent_dm(recipient, text)
return jsonify({
'success': True,
'message': 'DM sent',