新增 Google Chat 对接的全套工具模块,包括: - 会话线程管理、消息解析与格式化 - Pub/Sub 消息解码、提及和命令识别 - 权限审批、目录管理和审计日志 - 消息缓存、媒体上传下载和 SSFR 防护 - 策略配置、卡片构建和流式回复支持
340 lines
13 KiB
Python
340 lines
13 KiB
Python
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()
|