fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
249
backend/content_service/matrix_client.py
Normal file
249
backend/content_service/matrix_client.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
Matrix Client für Content Feed Publishing
|
||||
Publish Educational Content to Matrix Spaces/Rooms
|
||||
"""
|
||||
from matrix_nio import AsyncClient, MatrixRoom, RoomMessageText
|
||||
from typing import Optional, List, Dict
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MatrixContentClient:
|
||||
"""Matrix Client for Content Publishing"""
|
||||
|
||||
def __init__(self):
|
||||
self.homeserver = os.getenv("MATRIX_HOMESERVER", "http://localhost:8008")
|
||||
self.access_token = os.getenv("MATRIX_ACCESS_TOKEN")
|
||||
self.bot_user_id = os.getenv("MATRIX_BOT_USER", "@breakpilot-bot:localhost")
|
||||
|
||||
# Feed Rooms
|
||||
self.feed_room_id = os.getenv("MATRIX_FEED_ROOM", "!breakpilot-feed:localhost")
|
||||
self.movement_room = os.getenv("MATRIX_MOVEMENT_ROOM", "!movement:localhost")
|
||||
self.math_room = os.getenv("MATRIX_MATH_ROOM", "!math:localhost")
|
||||
self.steam_room = os.getenv("MATRIX_STEAM_ROOM", "!steam:localhost")
|
||||
|
||||
self.client: Optional[AsyncClient] = None
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to Matrix homeserver"""
|
||||
if not self.access_token:
|
||||
logger.warning("No Matrix access token configured - Matrix integration disabled")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.client = AsyncClient(self.homeserver, self.bot_user_id)
|
||||
self.client.access_token = self.access_token
|
||||
|
||||
# Test connection
|
||||
whoami = await self.client.whoami()
|
||||
logger.info(f"✅ Matrix connected as {whoami.user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Matrix connection failed: {e}")
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from Matrix"""
|
||||
if self.client:
|
||||
await self.client.close()
|
||||
|
||||
async def publish_content(
|
||||
self,
|
||||
content_id: str,
|
||||
title: str,
|
||||
description: str,
|
||||
content_type: str,
|
||||
category: str,
|
||||
license: str,
|
||||
creator_name: str,
|
||||
thumbnail_url: Optional[str] = None,
|
||||
download_url: Optional[str] = None,
|
||||
age_range: tuple = (6, 18),
|
||||
tags: List[str] = []
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Publish content to Matrix feed
|
||||
|
||||
Returns:
|
||||
Matrix event_id if successful, None otherwise
|
||||
"""
|
||||
if not self.client:
|
||||
logger.warning("Matrix client not connected")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Select room based on category
|
||||
room_id = self._get_room_for_category(category)
|
||||
|
||||
# Format message
|
||||
message = self._format_content_message(
|
||||
content_id=content_id,
|
||||
title=title,
|
||||
description=description,
|
||||
content_type=content_type,
|
||||
category=category,
|
||||
license=license,
|
||||
creator_name=creator_name,
|
||||
thumbnail_url=thumbnail_url,
|
||||
download_url=download_url,
|
||||
age_range=age_range,
|
||||
tags=tags
|
||||
)
|
||||
|
||||
# Send message
|
||||
response = await self.client.room_send(
|
||||
room_id=room_id,
|
||||
message_type="m.educational.content",
|
||||
content={
|
||||
"msgtype": "m.text",
|
||||
"body": message["plain"],
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": message["html"],
|
||||
"info": message["metadata"]
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"✅ Content published to Matrix: {content_id} → {room_id}")
|
||||
return response.event_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to publish content to Matrix: {e}")
|
||||
return None
|
||||
|
||||
def _get_room_for_category(self, category: str) -> str:
|
||||
"""Map content category to Matrix room"""
|
||||
category_map = {
|
||||
"movement": self.movement_room,
|
||||
"math": self.math_room,
|
||||
"steam": self.steam_room,
|
||||
}
|
||||
return category_map.get(category, self.feed_room_id)
|
||||
|
||||
def _format_content_message(
|
||||
self,
|
||||
content_id: str,
|
||||
title: str,
|
||||
description: str,
|
||||
content_type: str,
|
||||
category: str,
|
||||
license: str,
|
||||
creator_name: str,
|
||||
thumbnail_url: Optional[str],
|
||||
download_url: Optional[str],
|
||||
age_range: tuple,
|
||||
tags: List[str]
|
||||
) -> Dict[str, any]:
|
||||
"""Format content for Matrix message"""
|
||||
|
||||
# Content type emoji
|
||||
type_emoji = {
|
||||
"video": "📹",
|
||||
"pdf": "📄",
|
||||
"image_gallery": "🖼️",
|
||||
"markdown": "📝",
|
||||
"audio": "🎵",
|
||||
"h5p": "🎓"
|
||||
}.get(content_type, "📦")
|
||||
|
||||
# Category emoji
|
||||
category_emoji = {
|
||||
"movement": "🏃",
|
||||
"math": "🔢",
|
||||
"steam": "🔬",
|
||||
"language": "📖",
|
||||
"arts": "🎨",
|
||||
"social": "🤝",
|
||||
"mindfulness": "🧘"
|
||||
}.get(category, "📚")
|
||||
|
||||
# Plain text version
|
||||
plain = f"""
|
||||
{type_emoji} {title}
|
||||
|
||||
{description}
|
||||
|
||||
📝 Von: {creator_name}
|
||||
{category_emoji} Kategorie: {category}
|
||||
👥 Alter: {age_range[0]}-{age_range[1]} Jahre
|
||||
⚖️ Lizenz: {license}
|
||||
🏷️ Tags: {', '.join(tags) if tags else 'Keine'}
|
||||
|
||||
{download_url or f'https://breakpilot.app/content/{content_id}'}
|
||||
""".strip()
|
||||
|
||||
# HTML version with better formatting
|
||||
html = f"""
|
||||
<div class="breakpilot-content">
|
||||
<h3>{type_emoji} {title}</h3>
|
||||
<p>{description}</p>
|
||||
|
||||
{f'<img src="{thumbnail_url}" alt="{title}" style="max-width: 400px; border-radius: 8px;" />' if thumbnail_url else ''}
|
||||
|
||||
<div class="metadata">
|
||||
<p><strong>📝 Creator:</strong> {creator_name}</p>
|
||||
<p><strong>{category_emoji} Kategorie:</strong> {category}</p>
|
||||
<p><strong>👥 Altersgruppe:</strong> {age_range[0]}-{age_range[1]} Jahre</p>
|
||||
<p><strong>⚖️ Lizenz:</strong> <a href="https://creativecommons.org/licenses/{license.lower().replace('cc-', '').replace('-', '/')}/4.0/">{license}</a></p>
|
||||
{f'<p><strong>🏷️ Tags:</strong> {", ".join(tags)}</p>' if tags else ''}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a href="{download_url or f'https://breakpilot.app/content/{content_id}'}" style="background: #3498db; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
|
||||
📥 Inhalt ansehen/herunterladen
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
""".strip()
|
||||
|
||||
# Metadata for indexing & reactions
|
||||
metadata = {
|
||||
"content_id": content_id,
|
||||
"creator": creator_name,
|
||||
"license": license,
|
||||
"category": category,
|
||||
"content_type": content_type,
|
||||
"age_range": list(age_range),
|
||||
"tags": tags,
|
||||
"download_url": download_url,
|
||||
"thumbnail_url": thumbnail_url
|
||||
}
|
||||
|
||||
return {
|
||||
"plain": plain,
|
||||
"html": html,
|
||||
"metadata": metadata
|
||||
}
|
||||
|
||||
async def update_content_announcement(
|
||||
self,
|
||||
event_id: str,
|
||||
room_id: str,
|
||||
new_stats: Dict[str, any]
|
||||
):
|
||||
"""Update content message with new stats (downloads, ratings)"""
|
||||
try:
|
||||
# TODO: Edit message to add stats
|
||||
# Matrix nio doesn't support edit yet easily - would need to send m.room.message with m.replace
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update content announcement: {e}")
|
||||
|
||||
async def create_discussion_thread(
|
||||
self,
|
||||
content_id: str,
|
||||
room_id: str,
|
||||
event_id: str
|
||||
) -> Optional[str]:
|
||||
"""Create threaded discussion for content"""
|
||||
try:
|
||||
# TODO: Create thread (Matrix threading)
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create discussion thread: {e}")
|
||||
return None
|
||||
|
||||
# Global instance
|
||||
matrix_client = MatrixContentClient()
|
||||
Reference in New Issue
Block a user