[split-required] Split 700-870 LOC files across all services
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>
This commit is contained in:
405
backend-lehrer/messenger_conversations.py
Normal file
405
backend-lehrer/messenger_conversations.py
Normal file
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user