fix: WhatsApp @c.us Doppel-Suffix + Chat löschen in TUI
- 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
This commit is contained in:
@@ -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 sqlalchemy.orm import Session
|
||||||
|
|
||||||
from api.auth import require_api_key
|
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)
|
msgs = conversation_service.get_messages(db, conv_id, limit=limit, offset=offset)
|
||||||
conversation_service.mark_all_read(db, conv_id)
|
conversation_service.mark_all_read(db, conv_id)
|
||||||
return [MessageResponse.model_validate(m) for m in msgs]
|
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)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class WhatsAppChannel(BaseChannel):
|
|||||||
return {"success": False, "channel_message_id": None, "error": "WhatsApp not configured"}
|
return {"success": False, "channel_message_id": None, "error": "WhatsApp not configured"}
|
||||||
|
|
||||||
# chatId: Telefonnummer ohne + gefolgt von @c.us, z.B. "4917612345678@c.us"
|
# 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}"
|
url = f"{self._base_url}/sendMessage/{self._token}"
|
||||||
body: dict[str, Any] = {"chatId": chat_id, "message": text}
|
body: dict[str, Any] = {"chatId": chat_id, "message": text}
|
||||||
if reply_to_id:
|
if reply_to_id:
|
||||||
|
|||||||
@@ -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:
|
def mark_all_read(db: Session, conv_id: str) -> None:
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
db.query(Message).filter(
|
db.query(Message).filter(
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ class MCMApiClient:
|
|||||||
async def get_contacts(self) -> list[dict[str, Any]]:
|
async def get_contacts(self) -> list[dict[str, Any]]:
|
||||||
return await self._get("/contacts")
|
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:
|
async def get_telegram_qr(self, contact_id: str) -> bytes:
|
||||||
"""QR-Code PNG-Bytes für einen Kontakt abrufen."""
|
"""QR-Code PNG-Bytes für einen Kontakt abrufen."""
|
||||||
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class MainScreen(Screen):
|
|||||||
Binding("n", "new_message", "Neu"),
|
Binding("n", "new_message", "Neu"),
|
||||||
Binding("r", "refresh", "Aktualisieren"),
|
Binding("r", "refresh", "Aktualisieren"),
|
||||||
Binding("t", "telegram_qr", "Telegram QR"),
|
Binding("t", "telegram_qr", "Telegram QR"),
|
||||||
|
Binding("d", "delete_conv", "Chat löschen"),
|
||||||
Binding("q", "quit_app", "Beenden"),
|
Binding("q", "quit_app", "Beenden"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -241,6 +242,35 @@ class MainScreen(Screen):
|
|||||||
if self._current_conv_id:
|
if self._current_conv_id:
|
||||||
await self._load_messages(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:
|
def action_telegram_qr(self) -> None:
|
||||||
asyncio.create_task(self._generate_telegram_qr())
|
asyncio.create_task(self._generate_telegram_qr())
|
||||||
|
|
||||||
@@ -301,3 +331,22 @@ class MainScreen(Screen):
|
|||||||
|
|
||||||
def action_quit_app(self) -> None:
|
def action_quit_app(self) -> None:
|
||||||
self.app.exit()
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user