backend-lehrer (11 files): - llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6) - messenger_api.py (840 → 5), print_generator.py (824 → 5) - unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4) - llm_gateway/routes/edu_search_seeds.py (710 → 4) klausur-service (12 files): - ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4) - legal_corpus_api.py (790 → 4), page_crop.py (758 → 3) - mail/ai_service.py (747 → 4), github_crawler.py (767 → 3) - trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4) - dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4) website (6 pages): - audit-checklist (867 → 8), content (806 → 6) - screen-flow (790 → 4), scraper (789 → 5) - zeugnisse (776 → 5), modules (745 → 4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
406 lines
12 KiB
Python
406 lines
12 KiB
Python
"""
|
|
Messenger API - Conversation, Message, Group, Template & Stats Routes.
|
|
|
|
Conversations CRUD, message send/read, groups, templates, stats.
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
from messenger_models import (
|
|
Conversation,
|
|
Group,
|
|
GroupCreate,
|
|
Message,
|
|
MessageBase,
|
|
)
|
|
from messenger_helpers import (
|
|
DATA_DIR,
|
|
DEFAULT_TEMPLATES,
|
|
get_contacts,
|
|
get_conversations,
|
|
save_conversations,
|
|
get_messages,
|
|
save_messages,
|
|
get_groups,
|
|
save_groups,
|
|
load_json,
|
|
save_json,
|
|
)
|
|
|
|
router = APIRouter(tags=["Messenger"])
|
|
|
|
|
|
# ==========================================
|
|
# GROUPS ENDPOINTS
|
|
# ==========================================
|
|
|
|
@router.get("/groups", response_model=List[Group])
|
|
async def list_groups():
|
|
"""Listet alle Gruppen auf."""
|
|
return get_groups()
|
|
|
|
|
|
@router.post("/groups", response_model=Group)
|
|
async def create_group(group: GroupCreate):
|
|
"""Erstellt eine neue Gruppe."""
|
|
groups = get_groups()
|
|
|
|
now = datetime.utcnow().isoformat()
|
|
new_group = {
|
|
"id": str(uuid.uuid4()),
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
**group.dict()
|
|
}
|
|
|
|
groups.append(new_group)
|
|
save_groups(groups)
|
|
|
|
return new_group
|
|
|
|
|
|
@router.put("/groups/{group_id}/members")
|
|
async def update_group_members(group_id: str, member_ids: List[str]):
|
|
"""Aktualisiert die Mitglieder einer Gruppe."""
|
|
groups = get_groups()
|
|
group_idx = next((i for i, g in enumerate(groups) if g["id"] == group_id), None)
|
|
|
|
if group_idx is None:
|
|
raise HTTPException(status_code=404, detail="Gruppe nicht gefunden")
|
|
|
|
groups[group_idx]["member_ids"] = member_ids
|
|
groups[group_idx]["updated_at"] = datetime.utcnow().isoformat()
|
|
|
|
save_groups(groups)
|
|
return groups[group_idx]
|
|
|
|
|
|
@router.delete("/groups/{group_id}")
|
|
async def delete_group(group_id: str):
|
|
"""Loescht eine Gruppe."""
|
|
groups = get_groups()
|
|
groups = [g for g in groups if g["id"] != group_id]
|
|
save_groups(groups)
|
|
|
|
return {"status": "deleted", "id": group_id}
|
|
|
|
|
|
# ==========================================
|
|
# CONVERSATIONS ENDPOINTS
|
|
# ==========================================
|
|
|
|
@router.get("/conversations", response_model=List[Conversation])
|
|
async def list_conversations():
|
|
"""Listet alle Konversationen auf."""
|
|
conversations = get_conversations()
|
|
messages = get_messages()
|
|
|
|
# Unread count und letzte Nachricht hinzufuegen
|
|
for conv in conversations:
|
|
conv_messages = [m for m in messages if m.get("conversation_id") == conv["id"]]
|
|
conv["unread_count"] = len([m for m in conv_messages if not m.get("read") and m.get("sender_id") != "self"])
|
|
|
|
if conv_messages:
|
|
last_msg = max(conv_messages, key=lambda m: m.get("timestamp", ""))
|
|
conv["last_message"] = last_msg.get("content", "")[:50]
|
|
conv["last_message_time"] = last_msg.get("timestamp")
|
|
|
|
# Nach letzter Nachricht sortieren
|
|
conversations.sort(key=lambda c: c.get("last_message_time") or "", reverse=True)
|
|
|
|
return conversations
|
|
|
|
|
|
@router.post("/conversations", response_model=Conversation)
|
|
async def create_conversation(contact_id: Optional[str] = None, group_id: Optional[str] = None):
|
|
"""
|
|
Erstellt eine neue Konversation.
|
|
Entweder mit einem Kontakt (1:1) oder einer Gruppe.
|
|
"""
|
|
conversations = get_conversations()
|
|
|
|
if not contact_id and not group_id:
|
|
raise HTTPException(status_code=400, detail="Entweder contact_id oder group_id erforderlich")
|
|
|
|
# Pruefen ob Konversation bereits existiert
|
|
if contact_id:
|
|
existing = next((c for c in conversations
|
|
if not c.get("is_group") and contact_id in c.get("participant_ids", [])), None)
|
|
if existing:
|
|
return existing
|
|
|
|
now = datetime.utcnow().isoformat()
|
|
|
|
if group_id:
|
|
groups = get_groups()
|
|
group = next((g for g in groups if g["id"] == group_id), None)
|
|
if not group:
|
|
raise HTTPException(status_code=404, detail="Gruppe nicht gefunden")
|
|
|
|
new_conv = {
|
|
"id": str(uuid.uuid4()),
|
|
"name": group.get("name"),
|
|
"is_group": True,
|
|
"participant_ids": group.get("member_ids", []),
|
|
"group_id": group_id,
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
"last_message": None,
|
|
"last_message_time": None,
|
|
"unread_count": 0
|
|
}
|
|
else:
|
|
contacts = get_contacts()
|
|
contact = next((c for c in contacts if c["id"] == contact_id), None)
|
|
if not contact:
|
|
raise HTTPException(status_code=404, detail="Kontakt nicht gefunden")
|
|
|
|
new_conv = {
|
|
"id": str(uuid.uuid4()),
|
|
"name": contact.get("name"),
|
|
"is_group": False,
|
|
"participant_ids": [contact_id],
|
|
"group_id": None,
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
"last_message": None,
|
|
"last_message_time": None,
|
|
"unread_count": 0
|
|
}
|
|
|
|
conversations.append(new_conv)
|
|
save_conversations(conversations)
|
|
|
|
return new_conv
|
|
|
|
|
|
@router.get("/conversations/{conversation_id}", response_model=Conversation)
|
|
async def get_conversation(conversation_id: str):
|
|
"""Ruft eine Konversation ab."""
|
|
conversations = get_conversations()
|
|
conv = next((c for c in conversations if c["id"] == conversation_id), None)
|
|
|
|
if not conv:
|
|
raise HTTPException(status_code=404, detail="Konversation nicht gefunden")
|
|
|
|
return conv
|
|
|
|
|
|
@router.delete("/conversations/{conversation_id}")
|
|
async def delete_conversation(conversation_id: str):
|
|
"""Loescht eine Konversation und alle zugehoerigen Nachrichten."""
|
|
conversations = get_conversations()
|
|
conversations = [c for c in conversations if c["id"] != conversation_id]
|
|
save_conversations(conversations)
|
|
|
|
messages = get_messages()
|
|
messages = [m for m in messages if m.get("conversation_id") != conversation_id]
|
|
save_messages(messages)
|
|
|
|
return {"status": "deleted", "id": conversation_id}
|
|
|
|
|
|
# ==========================================
|
|
# MESSAGES ENDPOINTS
|
|
# ==========================================
|
|
|
|
@router.get("/conversations/{conversation_id}/messages", response_model=List[Message])
|
|
async def list_messages(
|
|
conversation_id: str,
|
|
limit: int = Query(50, ge=1, le=200),
|
|
before: Optional[str] = Query(None, description="Load messages before this timestamp")
|
|
):
|
|
"""Ruft Nachrichten einer Konversation ab."""
|
|
messages = get_messages()
|
|
conv_messages = [m for m in messages if m.get("conversation_id") == conversation_id]
|
|
|
|
if before:
|
|
conv_messages = [m for m in conv_messages if m.get("timestamp", "") < before]
|
|
|
|
# Nach Zeit sortieren (neueste zuletzt)
|
|
conv_messages.sort(key=lambda m: m.get("timestamp", ""))
|
|
|
|
return conv_messages[-limit:]
|
|
|
|
|
|
@router.post("/conversations/{conversation_id}/messages", response_model=Message)
|
|
async def send_message(conversation_id: str, message: MessageBase):
|
|
"""
|
|
Sendet eine Nachricht in einer Konversation.
|
|
|
|
Wenn send_email=True und der Kontakt eine Email-Adresse hat,
|
|
wird die Nachricht auch per Email versendet.
|
|
"""
|
|
conversations = get_conversations()
|
|
conv = next((c for c in conversations if c["id"] == conversation_id), None)
|
|
|
|
if not conv:
|
|
raise HTTPException(status_code=404, detail="Konversation nicht gefunden")
|
|
|
|
now = datetime.utcnow().isoformat()
|
|
|
|
new_message = {
|
|
"id": str(uuid.uuid4()),
|
|
"conversation_id": conversation_id,
|
|
"sender_id": "self",
|
|
"timestamp": now,
|
|
"read": True,
|
|
"read_at": now,
|
|
"email_sent": False,
|
|
"email_sent_at": None,
|
|
"email_error": None,
|
|
**message.dict()
|
|
}
|
|
|
|
# Email-Versand wenn gewuenscht
|
|
if message.send_email and not conv.get("is_group"):
|
|
# Kontakt laden
|
|
participant_ids = conv.get("participant_ids", [])
|
|
if participant_ids:
|
|
contacts = get_contacts()
|
|
contact = next((c for c in contacts if c["id"] == participant_ids[0]), None)
|
|
|
|
if contact and contact.get("email"):
|
|
try:
|
|
from email_service import email_service
|
|
|
|
result = email_service.send_messenger_notification(
|
|
to_email=contact["email"],
|
|
to_name=contact.get("name", ""),
|
|
sender_name="BreakPilot Lehrer",
|
|
message_content=message.content
|
|
)
|
|
|
|
if result.success:
|
|
new_message["email_sent"] = True
|
|
new_message["email_sent_at"] = result.sent_at
|
|
else:
|
|
new_message["email_error"] = result.error
|
|
|
|
except Exception as e:
|
|
new_message["email_error"] = str(e)
|
|
|
|
messages = get_messages()
|
|
messages.append(new_message)
|
|
save_messages(messages)
|
|
|
|
# Konversation aktualisieren
|
|
conv_idx = next(i for i, c in enumerate(conversations) if c["id"] == conversation_id)
|
|
conversations[conv_idx]["last_message"] = message.content[:50]
|
|
conversations[conv_idx]["last_message_time"] = now
|
|
conversations[conv_idx]["updated_at"] = now
|
|
save_conversations(conversations)
|
|
|
|
return new_message
|
|
|
|
|
|
@router.put("/messages/{message_id}/read")
|
|
async def mark_message_read(message_id: str):
|
|
"""Markiert eine Nachricht als gelesen."""
|
|
messages = get_messages()
|
|
msg_idx = next((i for i, m in enumerate(messages) if m["id"] == message_id), None)
|
|
|
|
if msg_idx is None:
|
|
raise HTTPException(status_code=404, detail="Nachricht nicht gefunden")
|
|
|
|
messages[msg_idx]["read"] = True
|
|
messages[msg_idx]["read_at"] = datetime.utcnow().isoformat()
|
|
save_messages(messages)
|
|
|
|
return {"status": "read", "id": message_id}
|
|
|
|
|
|
@router.put("/conversations/{conversation_id}/read-all")
|
|
async def mark_all_messages_read(conversation_id: str):
|
|
"""Markiert alle Nachrichten einer Konversation als gelesen."""
|
|
messages = get_messages()
|
|
now = datetime.utcnow().isoformat()
|
|
|
|
for msg in messages:
|
|
if msg.get("conversation_id") == conversation_id and not msg.get("read"):
|
|
msg["read"] = True
|
|
msg["read_at"] = now
|
|
|
|
save_messages(messages)
|
|
|
|
return {"status": "all_read", "conversation_id": conversation_id}
|
|
|
|
|
|
# ==========================================
|
|
# TEMPLATES ENDPOINTS
|
|
# ==========================================
|
|
|
|
@router.get("/templates")
|
|
async def list_templates():
|
|
"""Listet alle Nachrichtenvorlagen auf."""
|
|
templates_file = DATA_DIR / "templates.json"
|
|
if templates_file.exists():
|
|
templates = load_json(templates_file)
|
|
else:
|
|
templates = DEFAULT_TEMPLATES
|
|
save_json(templates_file, templates)
|
|
|
|
return templates
|
|
|
|
|
|
@router.post("/templates")
|
|
async def create_template(name: str, content: str, category: str = "custom"):
|
|
"""Erstellt eine neue Vorlage."""
|
|
templates_file = DATA_DIR / "templates.json"
|
|
templates = load_json(templates_file) if templates_file.exists() else DEFAULT_TEMPLATES.copy()
|
|
|
|
new_template = {
|
|
"id": str(uuid.uuid4()),
|
|
"name": name,
|
|
"content": content,
|
|
"category": category
|
|
}
|
|
|
|
templates.append(new_template)
|
|
save_json(templates_file, templates)
|
|
|
|
return new_template
|
|
|
|
|
|
@router.delete("/templates/{template_id}")
|
|
async def delete_template(template_id: str):
|
|
"""Loescht eine Vorlage."""
|
|
templates_file = DATA_DIR / "templates.json"
|
|
templates = load_json(templates_file) if templates_file.exists() else DEFAULT_TEMPLATES.copy()
|
|
|
|
templates = [t for t in templates if t["id"] != template_id]
|
|
save_json(templates_file, templates)
|
|
|
|
return {"status": "deleted", "id": template_id}
|
|
|
|
|
|
# ==========================================
|
|
# STATS ENDPOINT
|
|
# ==========================================
|
|
|
|
@router.get("/stats")
|
|
async def get_messenger_stats():
|
|
"""Gibt Statistiken zum Messenger zurueck."""
|
|
contacts = get_contacts()
|
|
conversations = get_conversations()
|
|
messages = get_messages()
|
|
groups = get_groups()
|
|
|
|
unread_total = sum(1 for m in messages if not m.get("read") and m.get("sender_id") != "self")
|
|
|
|
return {
|
|
"total_contacts": len(contacts),
|
|
"total_groups": len(groups),
|
|
"total_conversations": len(conversations),
|
|
"total_messages": len(messages),
|
|
"unread_messages": unread_total,
|
|
"contacts_by_role": {
|
|
role: len([c for c in contacts if c.get("role") == role])
|
|
for role in set(c.get("role", "parent") for c in contacts)
|
|
}
|
|
}
|