diff --git a/app/routers/messages.py b/app/routers/messages.py index a2557e6..32aa3dd 100644 --- a/app/routers/messages.py +++ b/app/routers/messages.py @@ -180,7 +180,7 @@ RESEND_WINDOW_SECONDS = 30 async def resend_channel_message( message_id: int, new_timestamp: bool = Query(default=False), -) -> dict[str, object]: +) -> ResendChannelMessageResponse: """Resend a channel message. When new_timestamp=False (default): byte-perfect resend using the original timestamp. diff --git a/app/services/message_send.py b/app/services/message_send.py index 1279ebb..8b41675 100644 --- a/app/services/message_send.py +++ b/app/services/message_send.py @@ -8,6 +8,7 @@ from typing import Any from fastapi import HTTPException from meshcore import EventType +from app.models import ResendChannelMessageResponse from app.region_scope import normalize_region_scope from app.repository import AppSettingsRepository, ContactRepository, MessageRepository from app.services.messages import ( @@ -437,7 +438,7 @@ async def resend_channel_message_record( now_fn: NowFn, temp_radio_slot: int, message_repository=MessageRepository, -) -> dict[str, Any]: +) -> ResendChannelMessageResponse: """Resend a stored outgoing channel message.""" try: key_bytes = bytes.fromhex(message.conversation_key) @@ -530,7 +531,11 @@ async def resend_channel_message_record( new_message.id, channel.name, ) - return {"status": "ok", "message_id": new_message.id, "message": new_message} + return ResendChannelMessageResponse( + status="ok", + message_id=new_message.id, + message=new_message, + ) logger.info("Resent channel message %d to %s", message.id, channel.name) - return {"status": "ok", "message_id": message.id} + return ResendChannelMessageResponse(status="ok", message_id=message.id) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index df40a82..fede7ae 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -20,6 +20,7 @@ import type { RadioDiscoveryResponse, RadioDiscoveryTarget, PathDiscoveryResponse, + ResendChannelMessageResponse, RepeaterAclResponse, RepeaterAdvertIntervalsResponse, RepeaterLoginResponse, @@ -34,12 +35,6 @@ import type { UnreadCounts, } from './types'; -export interface ResendChannelMessageResponse { - status: string; - message_id: number; - message?: Message; -} - const API_BASE = '/api'; async function fetchJson(url: string, options?: RequestInit): Promise { diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 128dab7..20fd895 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -247,6 +247,12 @@ export interface MessagesAroundResponse { has_newer: boolean; } +export interface ResendChannelMessageResponse { + status: string; + message_id: number; + message?: Message; +} + type ConversationType = 'contact' | 'channel' | 'raw' | 'map' | 'visualizer' | 'search'; export interface Conversation { diff --git a/tests/test_send_messages.py b/tests/test_send_messages.py index 6fee32e..99d4a17 100644 --- a/tests/test_send_messages.py +++ b/tests/test_send_messages.py @@ -632,8 +632,8 @@ class TestResendChannelMessage: ): result = await resend_channel_message(msg_id, new_timestamp=False) - assert result["status"] == "ok" - assert result["message_id"] == msg_id + assert result.status == "ok" + assert result.message_id == msg_id # Verify radio was called with correct timestamp bytes mc.commands.send_chan_msg.assert_awaited_once() @@ -731,7 +731,7 @@ class TestResendChannelMessage: ): result = await resend_channel_message(msg_id, new_timestamp=False) - assert result["status"] == "ok" + assert result.status == "ok" mock_broadcast_error.assert_called_once() assert "restore failed" in mock_broadcast_error.call_args.args[0].lower() @@ -762,15 +762,16 @@ class TestResendChannelMessage: mock_time.time.return_value = float(now) result = await resend_channel_message(msg_id, new_timestamp=True) - assert result["status"] == "ok" - assert result["message_id"] != msg_id - resent = await MessageRepository.get_by_id(result["message_id"]) + assert result.status == "ok" + assert result.message_id != msg_id + resent = await MessageRepository.get_by_id(result.message_id) assert resent is not None - assert result["message"].id == resent.id - assert result["message"].conversation_key == resent.conversation_key - assert result["message"].text == resent.text - assert result["message"].sender_timestamp == resent.sender_timestamp - assert result["message"].outgoing is True + assert result.message is not None + assert result.message.id == resent.id + assert result.message.conversation_key == resent.conversation_key + assert result.message.text == resent.text + assert result.message.sender_timestamp == resent.sender_timestamp + assert result.message.outgoing is True assert resent.sender_timestamp == now + 1 assert resent.received_at == now sent_timestamp = int.from_bytes( @@ -896,9 +897,9 @@ class TestResendChannelMessage: ): result = await resend_channel_message(msg_id, new_timestamp=True) - assert result["status"] == "ok" + assert result.status == "ok" # Should return a NEW message id, not the original - assert result["message_id"] != msg_id + assert result.message_id != msg_id @pytest.mark.asyncio async def test_resend_new_timestamp_creates_new_message(self, test_db): @@ -925,7 +926,7 @@ class TestResendChannelMessage: ): result = await resend_channel_message(msg_id, new_timestamp=True) - new_msg_id = result["message_id"] + new_msg_id = result.message_id new_msg = await MessageRepository.get_by_id(new_msg_id) original_msg = await MessageRepository.get_by_id(msg_id) @@ -963,7 +964,7 @@ class TestResendChannelMessage: mock_broadcast.assert_called_once() event_type, event_data = mock_broadcast.call_args.args assert event_type == "message" - assert event_data["id"] == result["message_id"] + assert event_data["id"] == result.message_id assert event_data["outgoing"] is True assert event_data["channel_name"] == "#broadcast"