"""Shared Slack + AWS I/O for the post and interactions Lambdas. Keeps the Block Kit builders (blockkit.py) pure: everything that touches the network or AWS lives here. Slack credentials are a single Secrets Manager secret ``{ botToken, signingSecret, channelId }``; the Grafana dashboard URL is operational config in SSM (editable without a redeploy). Both are cached for the life of the execution environment. """ from __future__ import annotations import json import os import boto3 from slack_sdk import WebClient from slack_sdk.signature import SignatureVerifier _secrets = boto3.client("secretsmanager") _ssm = boto3.client("ssm") _s3 = boto3.client("s3") _SECRET_NAME = os.environ["SLACK_SECRET_NAME"] _DASHBOARD_PARAM = os.environ["DASHBOARD_URL_PARAM"] _BUCKET = os.environ["ANALYTICS_BUCKET"] # Lazily-populated caches (warm across invocations in the same container). _creds: dict | None = None _dashboard_url: str | None = None def get_credentials() -> dict: """Return the Slack creds dict: ``botToken``, ``signingSecret``, ``channelId``.""" global _creds if _creds is None: raw = _secrets.get_secret_value(SecretId=_SECRET_NAME)["SecretString"] _creds = json.loads(raw) return _creds def get_dashboard_url() -> str: """Grafana dashboard URL for the 📊 button / modal overflow links (SSM).""" global _dashboard_url if _dashboard_url is None: _dashboard_url = _ssm.get_parameter(Name=_DASHBOARD_PARAM)["Parameter"]["Value"] return _dashboard_url def web_client() -> WebClient: return WebClient(token=get_credentials()["botToken"]) def channel_id() -> str: return get_credentials()["channelId"] def verify_signature(body: str, timestamp: str, signature: str) -> bool: """Validate a Slack request signature (HMAC + 5-minute replay window).""" verifier = SignatureVerifier(signing_secret=get_credentials()["signingSecret"]) return verifier.is_valid(body=body, timestamp=timestamp, signature=signature) def read_analytics_json(dt: str, name: str): """Read ``analytics/dt=
/`` as JSON, or None if absent.""" key = f"analytics/dt={dt}/{name}" try: obj = _s3.get_object(Bucket=_BUCKET, Key=key) except _s3.exceptions.NoSuchKey: return None return json.loads(obj["Body"].read())