"""Organization-Settings-Helper. KV-Store pro Organisation. Aktuell genutzt fuer: - output_language ('de'|'en'|...) - Anzeige-/Lagebild-Sprache - source_language_whitelist (JSON-Liste, z.B. ["ja"]) - schraenkt RSS/Telegram-Quellen ein - research_language (ISO-Code) - steuert WebSearch-Prompts (default = output_language) - translator_enabled ('true'|'false') - override fuer das globale TRANSLATOR_ENABLED-Flag Cache: TTL 60s in-memory pro (tenant_id, key). Wird bei set_org_setting() invalidiert. """ import json import logging import os import time from typing import Optional import aiosqlite logger = logging.getLogger("osint.org_settings") _CACHE: dict[tuple[int, str], tuple[float, Optional[str]]] = {} _TTL_SECONDS = 60.0 def _cache_get(tenant_id: int, key: str) -> tuple[bool, Optional[str]]: """(hit, value). hit=True heisst Cache traf; value kann auch None sein.""" entry = _CACHE.get((tenant_id, key)) if entry is None: return (False, None) expires_at, value = entry if time.monotonic() > expires_at: _CACHE.pop((tenant_id, key), None) return (False, None) return (True, value) def _cache_put(tenant_id: int, key: str, value: Optional[str]) -> None: _CACHE[(tenant_id, key)] = (time.monotonic() + _TTL_SECONDS, value) def _cache_invalidate(tenant_id: int, key: str) -> None: _CACHE.pop((tenant_id, key), None) async def get_org_setting( db: aiosqlite.Connection, tenant_id: int, key: str, default: Optional[str] = None, ) -> Optional[str]: """Liest ein Org-Setting. Fallback auf default.""" if tenant_id is None: return default hit, cached = _cache_get(tenant_id, key) if hit: return cached if cached is not None else default cursor = await db.execute( "SELECT value FROM organization_settings WHERE organization_id = ? AND key = ?", (tenant_id, key), ) row = await cursor.fetchone() value = row["value"] if row else None _cache_put(tenant_id, key, value) return value if value is not None else default async def set_org_setting( db: aiosqlite.Connection, tenant_id: int, key: str, value: str, ) -> None: """Setzt ein Org-Setting (upsert).""" await db.execute( """INSERT INTO organization_settings (organization_id, key, value, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(organization_id, key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP""", (tenant_id, key, value), ) await db.commit() _cache_invalidate(tenant_id, key) logger.info("Org %s Setting %s='%s' gespeichert", tenant_id, key, value) # Bekannte Sprachen + Anzeigenamen fuer Prompts LANGUAGE_DISPLAY_NAMES = { "de": "Deutsch", "en": "English", "ja": "Japanese", "zh": "Chinese", "ko": "Korean", "ru": "Russian", "ar": "Arabic", "fa": "Persian", "he": "Hebrew", "fr": "French", "es": "Spanish", } async def get_org_language( db: aiosqlite.Connection, tenant_id: int, ) -> str: """Liefert ISO-2-Sprachcode der Org (default 'de'). Steuert die Lagebild-/Anzeige-Sprache. """ value = await get_org_setting(db, tenant_id, "output_language", default="de") if value not in LANGUAGE_DISPLAY_NAMES: logger.warning("Unbekannte output_language '%s' fuer Org %s -- fallback 'de'", value, tenant_id) return "de" return value async def get_source_language_whitelist( db: aiosqlite.Connection, tenant_id: int, ) -> Optional[list[str]]: """Liefert Liste erlaubter Quellsprachen oder None (= keine Einschränkung). Gespeichert als JSON-Array unter dem Key 'source_language_whitelist'. Beispiel-Wert: '["ja"]' -> nur japanischsprachige Quellen. """ raw = await get_org_setting(db, tenant_id, "source_language_whitelist", default=None) if not raw: return None try: parsed = json.loads(raw) except (json.JSONDecodeError, TypeError) as e: logger.warning( "source_language_whitelist fuer Org %s ist kein JSON ('%s'): %s", tenant_id, raw, e, ) return None if not isinstance(parsed, list): logger.warning("source_language_whitelist fuer Org %s ist keine Liste: %r", tenant_id, parsed) return None cleaned = [str(x).strip().lower() for x in parsed if str(x).strip()] return cleaned or None async def get_research_language( db: aiosqlite.Connection, tenant_id: int, ) -> str: """Liefert die Sprache, in der der WebSearch-Researcher primär sucht. Default = output_language. Bei jp_demo z.B. 'ja', während output_language='de' bleibt. """ value = await get_org_setting(db, tenant_id, "research_language", default=None) if value and value in LANGUAGE_DISPLAY_NAMES: return value return await get_org_language(db, tenant_id) async def get_translator_enabled( db: aiosqlite.Connection, tenant_id: Optional[int], ) -> bool: """Liefert true wenn der (volle) Translator-Schritt fuer diese Org laufen soll. Hierarchie: 1. Org-Setting 'translator_enabled' ('true'/'false') gewinnt, wenn gesetzt. 2. Sonst: globales ENV-Flag TRANSLATOR_ENABLED (Default true im config.py). """ if tenant_id is not None: raw = await get_org_setting(db, tenant_id, "translator_enabled", default=None) if raw is not None: return str(raw).strip().lower() in ("true", "1", "yes", "on") env_value = os.environ.get("TRANSLATOR_ENABLED", "true").strip().lower() return env_value in ("true", "1", "yes", "on") def language_display(lang_iso: str) -> str: """ISO-Code -> Anzeigename fuer Prompts ('de' -> 'Deutsch').""" return LANGUAGE_DISPLAY_NAMES.get(lang_iso, lang_iso)