From 7a008470ef010a78fd97fe289e2c154b3376c973 Mon Sep 17 00:00:00 2001 From: "itdrui.de" Date: Fri, 13 Mar 2026 15:04:15 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20WhatsApp=20@c.us=20Doppel-Suffix=20+=20C?= =?UTF-8?q?hat=20l=C3=B6schen=20in=20TUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WhatsApp send: @c.us wird vor dem Anhängen entfernt (verhindert 4915..@c.us@c.us) - DELETE /api/v1/conversations/{id}: löscht Konversation + alle Nachrichten - TUI: Taste D öffnet Bestätigungsdialog zum Löschen des aktuellen Chats --- api/routes/conversations.py | 14 ++++++++- channels/whatsapp_channel.py | 2 +- services/conversation_service.py | 7 +++++ tui/api_client.py | 8 ++++++ tui/screens/main_screen.py | 49 ++++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/api/routes/conversations.py b/api/routes/conversations.py index 7e948f9..2da7c7b 100644 --- a/api/routes/conversations.py +++ b/api/routes/conversations.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from api.auth import require_api_key @@ -64,3 +64,15 @@ def get_messages( msgs = conversation_service.get_messages(db, conv_id, limit=limit, offset=offset) conversation_service.mark_all_read(db, conv_id) return [MessageResponse.model_validate(m) for m in msgs] + + +@router.delete("/{conv_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_conversation( + conv_id: str, + db: Session = Depends(get_db), + _: str = Depends(require_api_key), +): + conv = conversation_service.get_by_id(db, conv_id) + if not conv: + raise HTTPException(status_code=404, detail="Conversation not found") + conversation_service.delete(db, conv) diff --git a/channels/whatsapp_channel.py b/channels/whatsapp_channel.py index 999ed71..69a9881 100644 --- a/channels/whatsapp_channel.py +++ b/channels/whatsapp_channel.py @@ -37,7 +37,7 @@ class WhatsAppChannel(BaseChannel): return {"success": False, "channel_message_id": None, "error": "WhatsApp not configured"} # chatId: Telefonnummer ohne + gefolgt von @c.us, z.B. "4917612345678@c.us" - chat_id = recipient.lstrip("+") + "@c.us" + chat_id = recipient.replace("@c.us", "").lstrip("+") + "@c.us" url = f"{self._base_url}/sendMessage/{self._token}" body: dict[str, Any] = {"chatId": chat_id, "message": text} if reply_to_id: diff --git a/services/conversation_service.py b/services/conversation_service.py index b9e947a..b7dbc71 100644 --- a/services/conversation_service.py +++ b/services/conversation_service.py @@ -75,6 +75,13 @@ def unread_count(db: Session, conv_id: str) -> int: ) +def delete(db: Session, conv: Conversation) -> None: + """Löscht eine Konversation inkl. aller Nachrichten.""" + db.query(Message).filter(Message.conversation_id == conv.id).delete() + db.delete(conv) + db.commit() + + def mark_all_read(db: Session, conv_id: str) -> None: now = datetime.utcnow() db.query(Message).filter( diff --git a/tui/api_client.py b/tui/api_client.py index a057b59..12699b5 100644 --- a/tui/api_client.py +++ b/tui/api_client.py @@ -101,6 +101,14 @@ class MCMApiClient: async def get_contacts(self) -> list[dict[str, Any]]: return await self._get("/contacts") + async def delete_conversation(self, conv_id: str) -> None: + async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: + resp = await client.delete( + self._base + f"/conversations/{conv_id}", + headers=self._headers, + ) + resp.raise_for_status() + async def get_telegram_qr(self, contact_id: str) -> bytes: """QR-Code PNG-Bytes für einen Kontakt abrufen.""" async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: diff --git a/tui/screens/main_screen.py b/tui/screens/main_screen.py index 2b49626..28e3e22 100644 --- a/tui/screens/main_screen.py +++ b/tui/screens/main_screen.py @@ -25,6 +25,7 @@ class MainScreen(Screen): Binding("n", "new_message", "Neu"), Binding("r", "refresh", "Aktualisieren"), Binding("t", "telegram_qr", "Telegram QR"), + Binding("d", "delete_conv", "Chat löschen"), Binding("q", "quit_app", "Beenden"), ] @@ -241,6 +242,35 @@ class MainScreen(Screen): if self._current_conv_id: await self._load_messages(self._current_conv_id) + def action_delete_conv(self) -> None: + if not self._current_conv_id: + self.notify("Kein Chat ausgewählt.", severity="warning") + return + title = (self._current_conv or {}).get("title") or self._current_conv_id[:8] + self.app.push_screen( + _ConfirmScreen(f"Chat löschen?\n\n{title}\n\nAlle Nachrichten werden entfernt."), + self._delete_conv_callback, + ) + + def _delete_conv_callback(self, confirmed: bool) -> None: + if confirmed: + asyncio.create_task(self._do_delete_conv()) + + async def _do_delete_conv(self) -> None: + conv_id = self._current_conv_id + if not conv_id: + return + try: + await self._api.delete_conversation(conv_id) + self._current_conv_id = None + self._current_conv = None + self.query_one("#chat-header", Static).update("Kein Chat geöffnet") + self.query_one("#message-log", RichLog).clear() + await self._load_conversations() + self.notify("Chat gelöscht.") + except Exception as exc: + self.notify(f"Löschen fehlgeschlagen: {exc}", severity="error") + def action_telegram_qr(self) -> None: asyncio.create_task(self._generate_telegram_qr()) @@ -301,3 +331,22 @@ class MainScreen(Screen): def action_quit_app(self) -> None: self.app.exit() + + +class _ConfirmScreen(ModalScreen[bool]): + """Einfacher Ja/Nein-Dialog.""" + + def __init__(self, message: str) -> None: + super().__init__() + self._message = message + + def compose(self) -> ComposeResult: + from textual.widgets import Label + with Vertical(id="compose-dialog"): + yield Label(self._message, id="confirm-msg") + with Horizontal(id="compose-buttons"): + yield Button("Abbrechen", variant="default", id="btn-no") + yield Button("Löschen", variant="error", id="btn-yes") + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.dismiss(event.button.id == "btn-yes")