This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/content_service/matrix_client.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

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()