feat(x): X (Twitter) als Bezugsquelle pro Lage
X-Accounts werden analog zu Telegram als Quelle (source_type=x_account) konfiguriert und pro Lage ueber include_x zugeschaltet. Der Scraper (feeds/x_parser.py, twscrape) liest Account-Timelines, optional ueber einen HTTP-Proxy mit Fallback auf direkten Abruf ueber die Server-IP. - DB-Migration include_x, Pydantic-Modelle, incidents-Router - Orchestrator-X-Pipeline plus Haiku-Account-Vorselektion - sources-Router /x/validate, x_account-Typ in Stats und Frontend - Lage-Einstellungen: X-Toggle neben international und Telegram - twscrape als Abhaengigkeit Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -21,7 +21,7 @@ router = APIRouter(prefix="/api/incidents", tags=["incidents"])
|
||||
|
||||
INCIDENT_UPDATE_COLUMNS = {
|
||||
"title", "description", "type", "status", "refresh_mode",
|
||||
"refresh_interval", "refresh_start_time", "retention_days", "international_sources", "include_telegram", "visibility",
|
||||
"refresh_interval", "refresh_start_time", "retention_days", "international_sources", "include_telegram", "include_x", "visibility",
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ async def list_incidents(
|
||||
query = (
|
||||
"SELECT id, title, description, type, status, refresh_mode, refresh_interval, "
|
||||
"refresh_start_time, retention_days, visibility, "
|
||||
"international_sources, include_telegram, created_by, created_at, updated_at, "
|
||||
"international_sources, include_telegram, include_x, created_by, created_at, updated_at, "
|
||||
"CASE WHEN summary IS NOT NULL AND summary != '' THEN 1 ELSE 0 END AS has_summary "
|
||||
"FROM incidents WHERE tenant_id = ? AND (visibility = 'public' OR created_by = ?)"
|
||||
)
|
||||
@@ -120,9 +120,9 @@ async def create_incident(
|
||||
now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
|
||||
cursor = await db.execute(
|
||||
"""INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval,
|
||||
refresh_start_time, retention_days, international_sources, include_telegram, visibility,
|
||||
refresh_start_time, retention_days, international_sources, include_telegram, include_x, visibility,
|
||||
tenant_id, created_by, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
data.title,
|
||||
data.description,
|
||||
@@ -133,6 +133,7 @@ async def create_incident(
|
||||
data.retention_days,
|
||||
1 if data.international_sources else 0,
|
||||
1 if data.include_telegram else 0,
|
||||
1 if data.include_x else 0,
|
||||
data.visibility,
|
||||
tenant_id,
|
||||
current_user["id"],
|
||||
@@ -385,7 +386,7 @@ async def update_incident(
|
||||
for field, value in data.model_dump(exclude_none=True).items():
|
||||
if field not in INCIDENT_UPDATE_COLUMNS:
|
||||
continue
|
||||
if field in ("international_sources", "include_telegram"):
|
||||
if field in ("international_sources", "include_telegram", "include_x"):
|
||||
updates[field] = 1 if value else 0
|
||||
else:
|
||||
updates[field] = value
|
||||
|
||||
@@ -144,6 +144,7 @@ async def get_source_stats(
|
||||
"rss_feed": {"count": 0, "articles": 0},
|
||||
"web_source": {"count": 0, "articles": 0},
|
||||
"telegram_channel": {"count": 0, "articles": 0},
|
||||
"x_account": {"count": 0, "articles": 0},
|
||||
"excluded": {"count": 0, "articles": 0},
|
||||
}
|
||||
for row in rows:
|
||||
@@ -637,6 +638,30 @@ async def validate_telegram_channel(
|
||||
raise HTTPException(status_code=500, detail="Telegram-Validierung fehlgeschlagen")
|
||||
|
||||
|
||||
@router.post("/x/validate")
|
||||
async def validate_x_account(
|
||||
data: dict,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Prueft ob ein X-Account (Twitter) erreichbar ist und gibt Account-Info zurueck."""
|
||||
handle = data.get("handle", "").strip()
|
||||
if not handle:
|
||||
raise HTTPException(status_code=400, detail="handle ist erforderlich")
|
||||
|
||||
try:
|
||||
from feeds.x_parser import XParser
|
||||
parser = XParser()
|
||||
result = await parser.validate_account(handle)
|
||||
if result:
|
||||
return result
|
||||
raise HTTPException(status_code=404, detail="X-Account nicht erreichbar oder nicht gefunden")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("X-Validierung fehlgeschlagen: %s", e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="X-Validierung fehlgeschlagen")
|
||||
|
||||
|
||||
@router.post("/refresh-counts")
|
||||
async def trigger_refresh_counts(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren