from __future__ import annotations from typing import Any _SETUP_GUIDE = """ Google Chat Setup Guide ======================= 1. Go to Google Cloud Console → APIs & Services → Enable APIs Enable the "Google Chat API" 2. Go to IAM & Admin → Service Accounts Create a new service account or use an existing one Generate a JSON key and download it 3. Go to Google Chat API → Configuration Set up the bot: - App name: Your bot name - Avatar URL: (optional) - Description: Your bot description - Functionality: Enable "Receive messages" and "Join spaces and group conversations" - Connection settings: * App URL: https://your-domain.com/api/webhook/googlechat * (Or use Pub/Sub for production) 4. Configure the adapter with your service account credentials using one of the supported methods (file, inline, or environment variable). 5. Optional: Set audienceType to "project-number" if using project-number auth, and configure the audience accordingly. 6. Configure security policies: - dmPolicy: "open" | "pairing" | "allowlist" | "disabled" - groupPolicy: "open" | "allowlist" | "disabled" - allowFrom: list of users/spaces allowed to interact - requireMention: require @mention in group messages """ class SetupWizard: def __init__(self, adapter=None): self._adapter = adapter self._steps: list[dict[str, Any]] = [] self._current_step = 0 self._results: dict[str, Any] = {} @property def current_step(self) -> dict[str, Any] | None: if 0 <= self._current_step < len(self._steps): return self._steps[self._current_step] return None def start(self) -> dict[str, Any]: self._results = {} self._current_step = 0 self._steps = [ self._step_credential_mode(), self._step_credential_input(), self._step_audience_config(), self._step_policy_config(), self._step_review(), ] return {"step": self._current_step, "steps": len(self._steps), "data": self.current_step} def next(self, answer: Any = None) -> dict[str, Any]: if self._current_step > 0 and answer is not None: self._results[self._steps[self._current_step - 1]["id"]] = answer self._current_step += 1 if self._current_step >= len(self._steps): return {"step": self._current_step, "done": True, "config": self._build_config()} return {"step": self._current_step, "steps": len(self._steps), "data": self.current_step} def _step_credential_mode(self) -> dict[str, Any]: return { "id": "credential_mode", "question": "How would you like to provide service account credentials?", "type": "select", "options": [ { "id": "env_file", "label": "Environment (GOOGLE_SERVICE_ACCOUNT_FILE)", "description": ( "Set the path to a service account JSON key file " "via the GOOGLE_SERVICE_ACCOUNT_FILE environment variable" ), }, { "id": "env_json", "label": "Environment (GOOGLE_CHAT_SERVICE_ACCOUNT)", "description": ( "Paste the full service account JSON content " "into the GOOGLE_CHAT_SERVICE_ACCOUNT environment variable" ), }, { "id": "file", "label": "File (serviceAccountFile)", "description": "Specify the path to a service account JSON key file on disk in the config", }, { "id": "inline", "label": "Inline (serviceAccount)", "description": "Paste the full service account JSON content directly into the config", }, { "id": "secret_ref", "label": "Secret Reference (serviceAccountRef)", "description": "Reference a secret via the secret management system", }, ], } def _step_credential_input(self) -> dict[str, Any]: mode = self._results.get("credential_mode", "") if mode == "file": return { "id": "credential_value", "question": "Please provide the path to your Google Cloud service account JSON key file:", "type": "text", "required": True, "hint": "e.g., /etc/secrets/google-service-account.json or ~/.config/gcp-sa-key.json", } if mode == "inline": return { "id": "credential_value", "question": "Please paste the full content of your Google Cloud service account JSON key:", "type": "text", "required": True, "hint": ( "This file can be downloaded from Google Cloud Console " "→ IAM & Admin → Service Accounts → Create Key → JSON" ), } if mode == "secret_ref": return { "id": "credential_value", "question": "Please provide the secret reference identifier:", "type": "text", "required": True, "hint": "e.g., myapp/gcp/googlechat-sa-key", } return { "id": "credential_value", "question": ( "Credential will be read from environment variables. " "Ensure GOOGLE_CHAT_SERVICE_ACCOUNT or " "GOOGLE_SERVICE_ACCOUNT_FILE is set." ), "type": "info", "required": False, } def _step_audience_config(self) -> dict[str, Any]: return { "id": "audience_config", "question": "Configure audience validation for webhook JWT:", "type": "form", "fields": [ { "id": "audience_type", "question": "Audience type:", "type": "select", "options": [ {"id": "app-url", "label": "App URL (recommended)", "description": "Match the app's HTTPS URL"}, { "id": "project-number", "label": "Project Number", "description": "Match the GCP project number", }, ], "default": "app-url", }, { "id": "audience", "question": "Audience value:", "type": "text", "hint": "For app-url: your HTTPS app URL. For project-number: your GCP project number.", }, ], } def _step_policy_config(self) -> dict[str, Any]: return { "id": "policy_config", "question": "Configure access policies:", "type": "form", "fields": [ { "id": "dm_policy", "question": "DM Policy:", "type": "select", "options": [ {"id": "open", "label": "Open", "description": "Any user can DM the bot"}, {"id": "pairing", "label": "Pairing", "description": "Users must complete a pairing challenge"}, {"id": "allowlist", "label": "Allowlist", "description": "Only users in allowFrom can DM"}, {"id": "disabled", "label": "Disabled", "description": "No DM access"}, ], "default": "open", }, { "id": "group_policy", "question": "Group Policy:", "type": "select", "options": [ {"id": "open", "label": "Open", "description": "Bot responds in all groups"}, { "id": "allowlist", "label": "Allowlist", "description": "Only spaces in groupAllowFrom are active", }, {"id": "disabled", "label": "Disabled", "description": "No group access"}, ], "default": "open", }, { "id": "require_mention", "question": "Require @mention in groups?", "type": "boolean", "default": False, }, ], } def _step_review(self) -> dict[str, Any]: return { "id": "review", "question": "Review configuration before saving:", "type": "review", "summary": self._build_config(), } def _build_config(self) -> dict[str, Any]: config: dict[str, Any] = {} mode = self._results.get("credential_mode", "") credential_value = self._results.get("credential_value", "") if mode == "inline" and credential_value: config["serviceAccount"] = credential_value elif mode == "file" and credential_value: config["serviceAccountFile"] = credential_value elif mode == "secret_ref" and credential_value: config["serviceAccountRef"] = credential_value audience = self._results.get("audience_config", {}) if isinstance(audience, dict): if audience.get("audience_type"): config["audienceType"] = audience["audience_type"] if audience.get("audience"): config["audience"] = audience["audience"] policy = self._results.get("policy_config", {}) if isinstance(policy, dict): if policy.get("dm_policy"): config["dmPolicy"] = policy["dm_policy"] if policy.get("group_policy"): config["groupPolicy"] = policy["group_policy"] if policy.get("require_mention") is not None: config["requireMention"] = policy["require_mention"] config["enabled"] = True return config def wizard_prompt_credential_mode() -> dict[str, Any]: return { "step": "select_credential_mode", "question": "How would you like to provide service account credentials?", "options": [ { "id": "env_file", "label": "Environment (GOOGLE_SERVICE_ACCOUNT_FILE)", "description": ( "Set the path to a service account JSON key file " "via the GOOGLE_SERVICE_ACCOUNT_FILE environment variable" ), }, { "id": "env_json", "label": "Environment (GOOGLE_CHAT_SERVICE_ACCOUNT)", "description": ( "Paste the full service account JSON content " "into the GOOGLE_CHAT_SERVICE_ACCOUNT environment variable" ), }, { "id": "file", "label": "File (serviceAccountFile)", "description": "Specify the path to a service account JSON key file on disk", }, { "id": "inline", "label": "Inline (serviceAccount)", "description": "Paste the full service account JSON content directly into the config", }, ], } def wizard_prompt_file_path() -> dict[str, Any]: return { "step": "provide_credential_path", "question": "Please provide the path to your Google Cloud service account JSON key file:", "required": True, } def wizard_prompt_service_account_json() -> dict[str, Any]: return { "step": "provide_credential_json", "question": ( "Please paste the full content of your Google Cloud service account " "JSON key (or set the environment variable directly):" ), "required": True, "hint": ( "This file can be downloaded from Google Cloud Console " "→ IAM & Admin → Service Accounts → Create Key → JSON" ), } def wizard_validate_service_account(cred_json: dict) -> dict[str, Any]: errors: list[str] = [] if cred_json.get("type") != "service_account": errors.append("Invalid credential type: expected 'service_account'") if not cred_json.get("private_key"): errors.append("Missing 'private_key' field") if not cred_json.get("client_email"): errors.append("Missing 'client_email' field") if not cred_json.get("token_uri"): errors.append("Missing 'token_uri' field") return {"valid": len(errors) == 0, "errors": errors} def get_setup_guide() -> str: return _SETUP_GUIDE.strip()