4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
"""Telegram credential resolution shared by every caller that posts to Telegram.
|
|
|
|
Resolution rule:
|
|
|
|
* **bot token** — integration store -> ``TELEGRAM_BOT_TOKEN`` env -> system keyring
|
|
* **chat id** — explicit override -> integration store ``default_chat_id`` ->
|
|
``TELEGRAM_DEFAULT_CHAT_ID`` env
|
|
|
|
The store, env, and keyring fallbacks match the resolution order the scheduler
|
|
and onboarding wizard use, so credentials saved via ``opensre onboard`` or
|
|
``opensre integrations setup telegram`` work uniformly across the watchdog,
|
|
Hermes incident sinks, the Telegram send-message tool, and any other caller.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
|
|
from platform.common.errors import OpenSREError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TelegramCredentials:
|
|
"""Resolved Telegram bot token + target chat id."""
|
|
|
|
# repr=False so the auto-generated __repr__ does not leak the token into
|
|
# pytest assertion output, tracebacks, or structured log capture.
|
|
bot_token: str = field(repr=False)
|
|
chat_id: str = field()
|
|
|
|
|
|
def _telegram_store_config() -> dict[str, object]:
|
|
"""Return the Telegram integration's effective config, or ``{}``.
|
|
|
|
Reads the merged integration store + environment view used everywhere else
|
|
(investigation pipeline, scheduler). Returns an empty mapping when the store
|
|
is unavailable or has no Telegram integration so callers fall back to the
|
|
environment / keyring. Resolution is wrapped defensively: a malformed or
|
|
locked store must never crash the caller at startup.
|
|
"""
|
|
try:
|
|
from integrations.catalog import resolve_effective_integrations
|
|
|
|
entry = resolve_effective_integrations().get("telegram", {})
|
|
config = entry.get("config", {}) if isinstance(entry, dict) else {}
|
|
return config if isinstance(config, dict) else {}
|
|
except (ImportError, KeyError, TypeError, ValueError, OSError, RuntimeError) as exc:
|
|
logger.debug(
|
|
"Failed to resolve Telegram credentials from the store: %s", exc, exc_info=True
|
|
)
|
|
return {}
|
|
|
|
|
|
def _resolve_bot_token(store_config: dict[str, object]) -> str:
|
|
"""Resolve the bot token: store first, then ``TELEGRAM_BOT_TOKEN`` env, then keyring."""
|
|
store_token = str(store_config.get("bot_token") or "").strip()
|
|
if store_token:
|
|
return store_token
|
|
# resolve_env_credential checks the environment first, then the system
|
|
# keyring — so guided setup (which stores the token in the keyring) works.
|
|
from config.llm_credentials import resolve_env_credential
|
|
|
|
return resolve_env_credential("TELEGRAM_BOT_TOKEN").strip()
|
|
|
|
|
|
def _resolve_chat_id(store_config: dict[str, object], chat_id_override: str | None) -> str:
|
|
"""Resolve the chat id: explicit override, then store, then env."""
|
|
# Strip first so a whitespace-only override falls back consistently with an
|
|
# empty-string override, instead of raising a misleading "pass --chat-id"
|
|
# error after the caller already passed one.
|
|
stripped_override = chat_id_override.strip() if chat_id_override else ""
|
|
if stripped_override:
|
|
return stripped_override
|
|
store_chat_id = str(store_config.get("default_chat_id") or "").strip()
|
|
if store_chat_id:
|
|
return store_chat_id
|
|
return os.getenv("TELEGRAM_DEFAULT_CHAT_ID", "").strip()
|
|
|
|
|
|
def load_credentials_from_env(
|
|
*,
|
|
chat_id_override: str | None = None,
|
|
) -> TelegramCredentials:
|
|
"""Resolve Telegram credentials from the integration store, env, or keyring.
|
|
|
|
Raises :class:`OpenSREError` with a setup-friendly suggestion when either
|
|
half is missing.
|
|
"""
|
|
store_config = _telegram_store_config()
|
|
|
|
bot_token = _resolve_bot_token(store_config)
|
|
if not bot_token:
|
|
raise OpenSREError(
|
|
"TELEGRAM_BOT_TOKEN is not set.",
|
|
suggestion=(
|
|
"Configure Telegram with `opensre integrations setup telegram` "
|
|
"(or `opensre onboard`), or export TELEGRAM_BOT_TOKEN=<your-bot-token>. "
|
|
"Get a token from @BotFather on Telegram."
|
|
),
|
|
)
|
|
|
|
chat_id = _resolve_chat_id(store_config, chat_id_override)
|
|
if not chat_id:
|
|
raise OpenSREError(
|
|
"Telegram chat id is not set.",
|
|
suggestion=(
|
|
"Set a default chat id during `opensre integrations setup telegram`, "
|
|
"export TELEGRAM_DEFAULT_CHAT_ID=<chat-id>, or pass --chat-id and retry."
|
|
),
|
|
)
|
|
|
|
return TelegramCredentials(bot_token=bot_token, chat_id=chat_id)
|
|
|
|
|
|
def resolve_telegram_bot_token(
|
|
task_params: dict[str, str] | None = None,
|
|
) -> str:
|
|
"""Resolve the Telegram bot token from task params, store, env, or keyring.
|
|
|
|
Priority: task params > store config > env var > system keyring.
|
|
"""
|
|
if task_params:
|
|
token = task_params.get("bot_token", "").strip()
|
|
if token:
|
|
return token
|
|
|
|
store_config = _telegram_store_config()
|
|
return _resolve_bot_token(store_config)
|