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>
250 lines
7.9 KiB
Python
250 lines
7.9 KiB
Python
"""
|
|
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()
|