diff --git a/packetserver/http/routers/send.py b/packetserver/http/routers/send.py index c37a032..2b2cd7e 100644 --- a/packetserver/http/routers/send.py +++ b/packetserver/http/routers/send.py @@ -5,28 +5,36 @@ from typing import List from persistent.list import PersistentList from persistent.mapping import PersistentMapping from datetime import datetime -import uuid +import transaction from packetserver.http.dependencies import get_current_http_user from packetserver.http.auth import HttpUser -from packetserver.server.messages import Message # core Message class +from packetserver.server.messages import Message +from packetserver.common.util import is_valid_ax25_callsign router = APIRouter(prefix="/api/v1", tags=["messages"]) class SendMessageRequest(BaseModel): - to: List[str] = Field(..., description="List of recipient callsigns (uppercase) or ['ALL'] for bulletin") - subject: str = Field("", description="Optional subject line") + to: List[str] = Field(..., description="List of recipient callsigns or ['ALL'] for bulletin") text: str = Field(..., min_length=1, description="Message body text") @validator("to") def validate_to(cls, v): if not v: raise ValueError("At least one recipient required") - # Allow 'ALL' only as single bulletin - if len(v) > 1 and "ALL" in [x.upper() for x in v]: + + validated = [] + for call in v: + call_upper = call.upper().strip() + if not is_valid_ax25_callsign(call_upper): + raise ValueError(f"Invalid AX.25 callsign: {call}") + validated.append(call_upper) + + if len(validated) > 1 and "ALL" in validated: raise ValueError("'ALL' can only be used alone for bulletins") - return [x.upper() for x in v] + + return validated @router.post("/messages") @@ -36,6 +44,8 @@ async def send_message( ): from packetserver.runners.http_server import get_db_connection conn = get_db_connection() + root = conn.root() + if not current_user.is_rf_enabled(conn): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -44,35 +54,35 @@ async def send_message( username = current_user.username - from packetserver.runners.http_server import get_db_connection - conn = get_db_connection() - root = conn.root() - - # Prepare recipients tuple - to_tuple = tuple(payload.to) - if "ALL" in payload.to: + # Prepare recipients + to_list = payload.to + to_tuple = tuple(to_list) + if "ALL" in to_list: to_tuple = ("ALL",) - # Create new Message + is_bulletin = "ALL" in to_list + recipients = to_list if not is_bulletin else list(root.get('users', {}).keys()) + + # Create message using only supported core params new_msg = Message( + text=payload.text, msg_from=username, msg_to=to_tuple, - text=payload.text, - attachments=() # empty for now + attachments=() ) - # Deliver to all recipients (including sender for sent folder) - recipients = payload.to if "ALL" not in payload.to else list(root.get('users', {}).keys()) + # Deliver to recipients + always sender (sent folder) + messages_root = root.setdefault('messages', PersistentMapping()) + delivered_to = set() - for recip in set(recipients + [username]): # always copy to sender - if 'messages' not in root: - root['messages'] = PersistentMapping() - mailbox = root['messages'].setdefault(recip, persistent.list.PersistentList()) + for recip in set(recipients) | {username}: + mailbox = messages_root.setdefault(recip, PersistentList()) mailbox.append(new_msg) + mailbox._p_changed = True + delivered_to.add(recip) - # Persist - conn.root()["messages"]._p_changed = True - # Note: transaction.commit() not needed here—FastAPI/ZODB handles on response + messages_root._p_changed = True + transaction.commit() return { "status": "sent", @@ -80,5 +90,5 @@ async def send_message( "from": username, "to": list(to_tuple), "sent_at": new_msg.sent_at.isoformat() + "Z", - "subject": payload.subject + "recipients_delivered": len(delivered_to) } \ No newline at end of file