在 Google Chat 中收集和管理联系人

本教程介绍如何构建一个 Google Chat 应用, Google Chat 用户可管理自己的个人和业务联系人。收集 Chat 应用会提示用户完成 在卡片消息和对话框中添加联系表单。

查看 Chat 应用的实际应用:

  • 使用斜杠命令创建联系表单。
    图 1.通过 Chat 应用会响应 斜杠命令 /about,其中包含一条文本消息和 打开联系表单。
  • 对话框中的联系表单。
    图 2.通过 Chat 应用会打开一个对话框,供用户执行以下操作: 输入有关联系人的信息。
  • 确认并查看对话框。
    图 3.通过 Chat 应用会返回一个确认对话框, 以便用户可以先查看并确认相关信息 提交。
  • 用于确认新联系人的短信。
    图 4.在用户提交后 Chat 应用会发送一个不公开的 确认提交。
  • 卡片消息中的联系表单。
    图 5.通过 Chat 应用还会提示用户添加联系人 消息卡片。

前提条件

目标

架构

Chat 应用是使用 Google Apps 脚本构建的,并使用互动事件来处理和回复 Chat 用户。

以下是用户通常如何与 Chat 应用互动:

  1. 用户打开与 Chat 应用的私信对话,或将 Chat 应用添加到现有聊天室。

  2. Chat 应用会通过以下方式提示用户添加联系人: 创建联系表单并将其显示为 card 对象。为了显示联系表单,Chat 应用 以如下方式对用户做出响应:

    • 回复他人用“@”提及您的消息和私信时,显示卡片消息 包含联系表单。
    • 响应斜杠命令 /addContact,方法是使用 联系表单。
    • 响应斜杠命令 /about,并返回包含以下内容的文本消息: 点击添加联系人按钮可打开一个对话框,其中包含 联系表单。
  3. 看到联系表单时,用户输入联系信息 以下字段和 widget:

    • 姓氏和名字:a textInput 接受字符串的 widget。
    • 生日日期:一个仅接受日期的 dateTimePicker widget。
    • 联系人类型:一个包含单选按钮的 selectionInput widget,可让用户选择并提交单个字符串值(PersonalWork)。
    • 查看并提交按钮:一个包含 button widget 的 buttonList 数组,用户点击该 widget 即可提交其输入的值。
  4. Google Chat 应用会处理 CARD_CLICKED 互动事件,以处理用户输入的值,并在确认卡片中显示这些值。

  5. 用户查看确认卡片,然后点击提交按钮 最终确定联系信息

  6. Google Chat 应用会发送一条私密短信,确认已提交。

准备环境

本部分介绍如何为 Chat 应用。

创建 Google Cloud 项目

Google Cloud 控制台

  1. 在 Google Cloud 控制台中,依次选择“菜单”图标 > IAM 和管理> 创建项目

    转到“创建项目”

  2. Project Name 字段中,为项目输入一个描述性名称。

    可选:如需修改项目 ID,请点击修改。项目 ID 无法更改 创建项目后,请选择一个满足您需求的 ID 项目。

  3. 位置字段中,点击浏览以显示项目的可能位置。然后,点击选择
  4. 点击创建。Google Cloud 控制台会转到“信息中心”页面,并且您的项目已创建完毕 就会出现这种问题

gcloud CLI

在以下某个开发环境中,访问 Google Cloud CLI (gcloud):

  • Cloud Shell:如需使用已设置 gcloud CLI 的在线终端,请激活 Cloud Shell。
    激活 Cloud Shell
  • Local Shell:如需使用本地开发环境, 安装初始化 gcloud CLI
    如需创建 Cloud 项目,请使用 gcloud projects create 命令:
    gcloud projects create PROJECT_ID
    为要创建的项目设置 ID,从而替换 PROJECT_ID

设置身份验证和授权

Google Chat 应用需要您配置 OAuth 同意屏幕 用户可以在 Google Workspace 应用(包括 Google Chat。

在本教程中,您部署的 Chat 应用仅供测试和内部使用,因此可以为意见征求界面使用占位符信息。在发布 Chat 应用之前,请将 任何包含真实信息的占位信息

  1. 在 Google Cloud 控制台中,前往 菜单 > API 和服务 > OAuth 同意屏幕

    转到 OAuth 同意屏幕

  2. 用户类型下,选择内部,然后点击创建

  3. 应用名称中,输入 Contact Manager

  4. 用户支持电子邮件地址中,选择您的电子邮件地址或合适的 Google 群组。

  5. 开发者联系信息下方,输入您的电子邮件地址。

  6. 点击保存并继续

  7. 范围页面上,点击保存并继续。(Chat 应用不需要任何 OAuth 范围。)

  8. 查看摘要,然后点击返回信息中心

创建和部署 Chat 扩展应用

在下一部分,您将复制并更新整个 包含所有必需应用的 Apps 脚本项目 因此无需复制 粘贴每个文件。

您还可以选择在 GitHub 上查看整个项目。

在 GitHub 上查看

以下是对每个文件的概述:

main.gs

处理所有应用逻辑,包括关于用户何时发送 Chat 应用,点击 一条 Chat 应用消息,或者打开和关闭对话框。

查看 main.gs 个代码

apps-script/contact-form-app/main.gs
/**
 * Copyright 2024 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Responds to a MESSAGE interaction event in Google Chat.
 *
 * @param {Object} event the MESSAGE interaction event from Chat API.
 * @return {Object} message response that opens a dialog or sends private
 *                          message with text and card.
 */
function onMessage(event) {
  if (event.message.slashCommand) {
    switch (event.message.slashCommand.commandId) {
      case 1:
        // If the slash command is "/about", responds with a text message and button
        // that opens a dialog.
        return {
          text: "Manage your personal and business contacts 📇. To add a " +
                  "contact, use the slash command `/addContact`.",
          accessoryWidgets: [{ buttonList: { buttons: [{
            text: "Add Contact",
            onClick: { action: {
              function: "openDialog",
              interaction: "OPEN_DIALOG"
            }}
          }]}}]
        }
      case 2:
        // If the slash command is "/addContact", opens a dialog.
        return openDialog(event);
    }
  }

  // If user sends the Chat app a message without a slash command, the app responds
  // privately with a text and card to add a contact.
  return {
    privateMessageViewer: event.user,
    text: "To add a contact, try `/addContact` or complete the form below:",
    cardsV2: [{
      cardId: "addContactForm",
      card: {
        header: { title: "Add a contact" },
        sections:[{ widgets: CONTACT_FORM_WIDGETS.concat([{
          buttonList: { buttons: [{
            text: "Review and submit",
            onClick: { action: { function : "openNextCard" }}
          }]}
        }])}]
      }
    }]
  };
}

/**
 * Responds to CARD_CLICKED interaction events in Google Chat.
 *
 * @param {Object} event the CARD_CLICKED interaction event from Google Chat.
 * @return {Object} message responses specific to the dialog handling.
 */
function onCardClick(event) {
  // Initial dialog form page
  if (event.common.invokedFunction === "openDialog") {
    return openDialog(event);
  // Second dialog form page
  } else if (event.common.invokedFunction === "openNextCard") {
    return openNextCard(
      event.user,
      fetchFormValue(event, "contactName"),
      fetchFormValue(event, "contactBirthdate"),
      fetchFormValue(event, "contactType"),
      event.isDialogEvent
    );
  // Dialog form submission
  } else if (event.common.invokedFunction === "submitForm") {
    const userInputs = event.common.parameters;
    return submitForm(event.user, userInputs, event.dialogEventType);
  }
}

/**
 * Extracts form input value for a given widget.
 *
 * @param {Object} event the CARD_CLICKED interaction event from Google Chat.
 * @param {String} widgetName a unique ID for the widget, specified in the widget's name field.
 * @returns the value inputted by the user, null if no value can be found.
 */
function fetchFormValue(event, widgetName) {
  const formItem = event.common.formInputs[widgetName][""];
  // For widgets that receive StringInputs data, the value input by the user.
  if (formItem.hasOwnProperty("stringInputs")) {
    const stringInput = event.common.formInputs[widgetName][""].stringInputs.value[0];
    if (stringInput != null) {
      return stringInput;
    }
  // For widgets that receive dateInput data, the value input by the user.
  } else if (formItem.hasOwnProperty("dateInput")) {
    const dateInput = event.common.formInputs[widgetName][""].dateInput.msSinceEpoch;
     if (dateInput != null) {
       return dateInput;
     }
  }

  return null;
}

/**
 * Opens a dialog that prompts users to add details about a contact.
 *
 * @return {Object} a message with an action response to open a dialog.
 */
function openDialog() {
  return { actionResponse: {
    type: "DIALOG",
    dialogAction: { dialog: { body: { sections: [{
      header: "Add new contact",
      widgets: CONTACT_FORM_WIDGETS.concat([{
        buttonList: { buttons: [{
          text: "Review and submit",
          onClick: { action: { function: "openNextCard" }}
        }]}
      }])
    }]}}}
  }};
}

/**
 * Returns a dialog or card message that displays a confirmation of contact
 * details before users submit.
 *
 * @param {String} user the user who submitted the information.
 * @param {String} contactName the contact name from the previous dialog or card.
 * @param {String} contactBirthdate the birthdate from the previous dialog or card.
 * @param {String} contactType the contact type from the previous dialog or card.
 * @param {boolean} fromDialog whether the information was submitted from a dialog.
 *
 * @return {Object} returns a dialog or private card message.
 */
function openNextCard(user, contactName, contactBirthdate, contactType, fromDialog) {
  const name = contactName ?? "<i>Not provided</i>";
  const birthdate = contactBirthdate ?? "<i>Not provided</i>";
  const type = contactType ?? "<i>Not provided</i>";
  const cardConfirmation = {
    header: "Your contact",
    widgets: [{
      textParagraph: { text: "Confirm contact information and submit:" }}, {
      textParagraph: { text: "<b>Name:</b> " + name }}, {
      textParagraph: {
        text: "<b>Birthday:</b> " + convertMillisToDateString(birthdate)
      }}, {
      textParagraph: { text: "<b>Type:</b> " + type }}, {
      buttonList: { buttons: [{
        text: "Submit",
        onClick: { action: {
          function: "submitForm",
          parameters: [{
            key: "contactName", value: name }, {
            key: "contactBirthdate", value: birthdate }, {
            key: "contactType", value: type
          }]
        }}
      }]}
    }]
  };

  // Returns a dialog with contact information that the user input.
  if (fromDialog) {
    return { action_response: {
      type: "DIALOG",
      dialogAction: { dialog: { body: { sections: [ cardConfirmation ]}}}
    }};
  }

  // Updates existing card message with contact information that the user input.
  return {
    actionResponse: { type: "UPDATE_MESSAGE" },
    privateMessageViewer: user,
    cardsV2: [{
      card: { sections: [cardConfirmation]}
    }]
  }
}

/**
  * Submits information from a dialog or card message.
  *
  * @param {Object} user the person who submitted the information.
  * @param {Object} userInputs the form input values from event parameters.
  * @param {boolean} dialogEventType "SUBMIT_DIALOG" if from a dialog.
  * @return {Object} a message response that opens a dialog or posts a private
  *                  message.
  */
function submitForm(user, userInputs, dialogEventType) {
  const contactName = userInputs["contactName"];
  // Checks to make sure the user entered a contact name.
  // If no name value detected, returns an error message.
  if (!contactName) {
    const errorMessage = "Don't forget to name your new contact!";
    if (dialogEventType === "SUBMIT_DIALOG") {
      return { actionResponse: {
        type: "DIALOG",
        dialogAction: { actionStatus: {
          statusCode: "INVALID_ARGUMENT",
          userFacingMessage: errorMessage
        }}
      }};
    } else {
      return {
        privateMessageViewer: user,
        text: errorMessage
      };
    }
  }

  // The Chat app indicates that it received form data from the dialog or card.
  // Sends private text message that confirms submission.
  const confirmationMessage = "✅ " + contactName + " has been added to your contacts.";
  if (dialogEventType === "SUBMIT_DIALOG") {
    return {
      actionResponse: {
        type: "NEW_MESSAGE",
        dialogAction: { actionStatus: {
          statusCode: "OK",
          userFacingMessage: "Success " + JSON.stringify(contactName)
        }}
      },
      privateMessageViewer: user,
      text: confirmationMessage
    }
  } else {
    return {
      actionResponse: { type: "NEW_MESSAGE" },
      privateMessageViewer: user,
      text: confirmationMessage
    };
  }
}

/**
 * Converts date in milliseconds since epoch to user-friendly string.
 *
 * @param {Object} millis the milliseconds since epoch time.
 * @return {string} Display-friend date (English US).
 */
function convertMillisToDateString(millis) {
  const date = new Date(millis);
  const options = { year: 'numeric', month: 'long', day: 'numeric' };
  return date.toLocaleDateString('en-US', options);
}
contactForm.gs

包含接收用户表单数据的微件。这些表单输入 微件显示在消息和对话框中的卡片中。

查看 contactForm.gs 代码

apps-script/contact-form-app/contactForm.gs
/**
 * Copyright 2024 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * The section of the contact card that contains the form input widgets. Used in a dialog and card message.
 * To add and preview widgets, use the Card Builder: https://addons.gsuite.google.com/uikit/builder
 */
const CONTACT_FORM_WIDGETS = [
  {
    "textInput": {
      "name": "contactName",
      "label": "First and last name",
      "type": "SINGLE_LINE"
    }
  },
  {
    "dateTimePicker": {
      "name": "contactBirthdate",
      "label": "Birthdate",
      "type": "DATE_ONLY"
    }
  },
  {
    "selectionInput": {
      "name": "contactType",
      "label": "Contact type",
      "type": "RADIO_BUTTON",
      "items": [
        {
          "text": "Work",
          "value": "Work",
          "selected": false
        },
        {
          "text": "Personal",
          "value": "Personal",
          "selected": false
        }
      ]
    }
  }
];
appsscript.json

通过 Apps 脚本清单 ,该脚本定义和配置 Apps 脚本项目, Chat 应用。

查看 appsscript.json 代码

apps-script/contact-form-app/appsscript.json
{
  "timeZone": "America/Los_Angeles",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "chat": {}
}

查找您的 Cloud 项目编号和 ID

  1. 在 Google Cloud 控制台中,前往您的 Cloud 项目。

    转到 Google Cloud 控制台

  2. 依次点击“设置和实用程序”图标 > 项目设置

  3. 记下项目编号项目 ID 字段中的值。您 请在以下各部分中使用它们。

创建 Apps 脚本项目

要创建 Apps 脚本项目并将其与您的 Cloud 项目:

  1. 点击以下按钮,打开 Manage contacts in Google Chat Apps Script 项目。
    打开项目
  2. 点击 Overview(概览)。
  3. 在概览页面上,点击 用于创建副本的图标 复制
  4. 为您的 Apps 脚本项目副本命名:

    1. 点击“在 Google Chat 中管理联系人”的副本

    2. 项目名称中,输入 Contact Manager - Google Chat app

    3. 点击重命名

设置 Apps 脚本项目的 Cloud 项目

  1. 在您的 Apps 脚本项目中, 点击 项目设置的图标 项目设置
  2. Google Cloud Platform (GCP) 项目下,点击更改项目
  3. GCP 项目编号中,粘贴您的 Cloud 项目的项目编号。
  4. 点击设置项目。Cloud 项目和 Apps 脚本 项目现已连接。

创建 Apps 脚本部署

现在,所有代码都已到位,接下来部署 Apps 脚本项目。在 Google Cloud 中配置 Chat 应用时,您需要使用部署 ID。

  1. 在 Apps 脚本中,打开 Chat 应用的项目。

    前往 Apps 脚本

  2. 依次点击 Deploy(部署)> New deployment(新建部署)。

  3. 如果尚未选择插件,请点击选择类型旁边的部署类型 项目设置的图标,然后选择插件

  4. 说明中,输入此版本的说明,例如 Test of Contact Manager

  5. 点击部署。Apps 脚本报告成功 并提供部署 ID。

  6. 点击 Copy 复制部署 ID,然后点击 Done

在 Google Cloud 控制台中配置 Chat 应用

本部分介绍了如何在 Google Cloud 控制台中使用 Chat 应用的相关信息(包括您刚刚通过 Apps Script 项目创建的部署的 ID)配置 Google Chat API。

  1. 在 Google Cloud 控制台中,点击菜单 &gt; 更多产品 &gt; Google Workspace &gt; 产品库 &gt; Google Chat API &gt; 管理 &gt; 配置

    前往 Chat API 配置

  2. 应用名称中,输入 Contact Manager

  3. 头像网址中,输入 https://developers.google.com/chat/images/contact-icon.png

  4. 说明中,输入 Manage your personal and business contacts

  5. 点击启用互动功能开关,将其切换到“开启”位置。

  6. 功能下,选中接收一对一消息加入聊天室和群组对话复选框。

  7. 连接设置下,选择 Apps 脚本

  8. 部署 ID 中,粘贴 Apps 脚本部署 ID 您在创建 Apps 脚本部署。

  9. 斜杠命令下,设置斜杠命令 /about/addContact:

    1. 点击添加斜杠命令以设置第一个斜杠命令。
    2. 名称中,输入 /about
    3. 命令 ID 中,输入 1
    4. 说明中,输入 Learn how to use this Chat app to manage your contacts
    5. 选择打开对话框
    6. 点击完成
    7. 点击添加斜杠命令以设置另一个斜杠命令。
    8. 名称中,输入 /addContact
    9. 命令 ID 中,输入 2
    10. 说明中,输入 Submit information about a contact
    11. 选择打开对话框
    12. 点击完成
  10. 公开范围下,选择 将此聊天应用设为可供 YOUR DOMAIN 中的特定人员和群组使用复选框,然后输入您的电子邮件地址。

  11. 日志下,选择将错误记录到 Logging

  12. 点击保存。系统会显示一条已保存配置的消息。

Chat 应用已准备好在 Chat 中安装和测试。

测试 Chat 应用

如需测试 Chat 应用,请使用 Chat 应用打开私信聊天室并发送消息:

  1. 使用您用于登录的 Google Workspace 账号打开 Google Chat 在您添加为可信测试员时提供的凭据。

    前往 Google Chat

  2. 点击 发起新对话
  3. 添加 1 人或更多人字段中,输入 Chat 应用的名称。
  4. 从结果中选择您的 Chat 应用。直接客户 消息会打开。

  1. 在与 Chat 应用的新私信对话中, 输入 /addContact,然后按 Enter 键。

  2. 在随即打开的对话框中,输入联系信息:

    1. 名字和姓氏文本字段中,输入名称。
    2. 生日日期选择器中,选择一个日期。
    3. 联系人类型下,选择工作个人单选按钮。
  3. 点击查看并提交

  4. 在确认对话框中,检查您提交的信息并 点击提交。Chat 应用会回复一条内容为 CONTACT NAME has been added to your contacts. 的文本消息。

  5. (可选)您还可以按照以下方式测试并提交联系表单 方式:

    • 使用 /about 斜线命令。聊天应用使用文本消息和显示 Add a contact 的配件 widget 按钮进行回复。您可以点击该按钮,打开包含联系表单的对话框。
    • 向 Chat 应用发送不带斜杠命令(例如 Hello)的私信。Chat 应用会回复一条文本和一张包含联系表单的卡片。

清理

为避免系统因本教程中使用的资源向您的 Google Cloud 账号收取费用,我们建议您删除该 Cloud 项目。

  1. 在 Google Cloud 控制台中,前往管理资源页面。点击 菜单 &gt; IAM 和管理员 &gt; 管理资源

    前往 Resource Manager

  2. 在项目列表中,选择要删除的项目,然后点击 删除
  3. 在对话框中输入项目 ID,然后点击关停以删除项目 项目。