设置和接收推送通知

您可以使用 Watches 集合中的方法在表单中的数据发生更改时接收通知。本页简要介绍了推送通知的概念,并提供了有关如何设置和接收推送通知的说明。

概览

借助 Google Forms API 推送通知功能,应用可以在表单中的数据发生更改时订阅通知。通知会发送到 Cloud Pub/Sub 主题,通常在更改发生后几分钟内发送。

如需接收推送通知,您需要设置 Cloud Pub/Sub 主题,并在为相应事件类型创建监听器时提供该主题的名称。

以下是本文件中使用的关键概念的定义:

  • 目标是指通知的发送位置。唯一支持的目标是 Cloud Pub/Sub 主题。
  • 事件类型是指第三方应用可以订阅的通知类别。
  • 监听是指向 Forms API 发出的指令,用于将特定表单上特定事件类型的通知传递给目标

在特定表单上为某个事件类型创建监听器后,该监听器的目标(即 Cloud Pub/Sub 主题)会接收来自该表单上这些事件的通知,直到监听器过期。手表的使用期限为一周,但您可以在手表过期前的任意时间通过向 watches.renew() 发出请求来延长使用期限。

您的 Cloud Pub/Sub 主题只会收到您使用所提供的凭据可以查看的表单的相关通知。例如,如果用户撤消了您应用的权限,或者失去了对受监控表单的修改权限,系统将不再发送通知。

可用的活动类型

Google Forms API 目前提供两类事件:

  • EventType.SCHEMA,用于通知表单内容和设置的更改。
  • EventType.RESPONSES,用于在提交表单回复(包括新回复和更新的回复)时发送通知。

通知响应

通知采用 JSON 编码,并包含:

  • 触发表单的 ID
  • 触发观看的 ID
  • 触发通知的事件类型
  • 由 Cloud Pub/Sub 设置的其他字段,例如 messageIdpublishTime

通知包含详细的表单或回答数据。每次收到通知后,都需要单独进行 API 调用来获取新数据。如需了解如何实现此目的,请参阅建议的用法

以下代码段展示了架构变更的示例通知:

{
  "attributes": {
    "eventType": "SCHEMA",
    "formId": "18Xgmr4XQb-l0ypfCNGQoHAw2o82foMr8J0HPHdagS6g",
    "watchId": "892515d1-a902-444f-a2fe-42b718fe8159"
  },
  "messageId": "767437830649",
  "publishTime": "2021-03-31T01:34:08.053Z"
}

以下代码段展示了有关新回答的示例通知:

{
  "attributes": {
    "eventType": "RESPONSES",
    "formId": "18Xgmr4XQb-l0ypfCNGQoHAw2o82foMr8J0HPHdagS6g",
    "watchId": "5d7e5690-b1ff-41ce-8afb-b469912efd7d"
  },
  "messageId": "767467004397",
  "publishTime": "2021-03-31T01:43:57.285Z"
}

设置 Cloud Pub/Sub 主题

通知会传递到 Cloud Pub/Sub 主题。通过 Cloud Pub/Sub,您可以通过 Web 钩子或轮询订阅端点来接收通知。

如需设置 Cloud Pub/Sub 主题,请执行以下操作:

  1. 完成 Cloud Pub/Sub 前提条件
  2. 设置 Cloud Pub/Sub 客户端
  3. 查看 Cloud Pub/Sub 定价,并为您的开发者控制台项目启用结算功能。
  4. 您可以通过以下三种方式之一创建 Cloud Pub/Sub 主题:

  5. 在 Cloud Pub/Sub 中创建订阅,以告知 Cloud Pub/Sub 如何传送通知。

  6. 最后,在创建以您的主题为目标的监听之前,您需要向 Google 表单通知服务账号 (forms-notifications@system.gserviceaccount.com) 授予权限,以便其可以向您的主题发布内容。

创建手表

获得 Forms API 推送通知服务账号可以发布到的主题后,您就可以使用 watches.create() 方法创建通知了。此方法会验证推送通知服务账号是否可以访问所提供的 Cloud Pub/Sub 主题,如果无法访问该主题(例如,如果该主题不存在,或者您未向其授予该主题的发布权限),则会失败。

Python

forms/snippets/create_watch.py
from apiclient import discovery
from httplib2 import Http
from oauth2client import client, file, tools

SCOPES = "https://www.googleapis.com/auth/drive"
DISCOVERY_DOC = "https://forms.googleapis.com/$discovery/rest?version=v1"

store = file.Storage("token.json")
creds = None
if not creds or creds.invalid:
  flow = client.flow_from_clientsecrets("client_secret.json", SCOPES)
  creds = tools.run_flow(flow, store)

service = discovery.build(
    "forms",
    "v1",
    http=creds.authorize(Http()),
    discoveryServiceUrl=DISCOVERY_DOC,
    static_discovery=False,
)

watch = {
    "watch": {
        "target": {"topic": {"topicName": "<YOUR_TOPIC_PATH>"}},
        "eventType": "RESPONSES",
    }
}

form_id = "<YOUR_FORM_ID>"

# Print JSON response after form watch creation
result = service.forms().watches().create(formId=form_id, body=watch).execute()
print(result)

Node.js

forms/snippets/create_watch.js
import path from 'node:path';
import {authenticate} from '@google-cloud/local-auth';
import {forms} from '@googleapis/forms';

// TODO: Replace with a valid form ID.
const formID = '<YOUR_FORM_ID>';

/**
 * Creates a watch on a form to get notifications for new responses.
 */
async function createWatch() {
  // Authenticate with Google and get an authorized client.
  const authClient = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/drive',
  });

  // Create a new Forms API client.
  const formsClient = forms({
    version: 'v1',
    auth: authClient,
  });

  // The request body to create a watch.
  const watchRequest = {
    watch: {
      target: {
        topic: {
          // TODO: Replace with a valid Cloud Pub/Sub topic name.
          topicName: 'projects/<YOUR_TOPIC_PATH>',
        },
      },
      // The event type to watch for. 'RESPONSES' is the only supported type.
      eventType: 'RESPONSES',
    },
  };

  // Send the request to create the watch.
  const result = await formsClient.forms.watches.create({
    formId: formID,
    requestBody: watchRequest,
  });

  console.log(result.data);
  return result.data;
}

删除手表

Python

forms/snippets/delete_watch.py
from apiclient import discovery
from httplib2 import Http
from oauth2client import client, file, tools

SCOPES = "https://www.googleapis.com/auth/drive"
DISCOVERY_DOC = "https://forms.googleapis.com/$discovery/rest?version=v1"

store = file.Storage("token.json")
creds = None
if not creds or creds.invalid:
  flow = client.flow_from_clientsecrets("client_secret.json", SCOPES)
  creds = tools.run_flow(flow, store)
service = discovery.build(
    "forms",
    "v1",
    http=creds.authorize(Http()),
    discoveryServiceUrl=DISCOVERY_DOC,
    static_discovery=False,
)

form_id = "<YOUR_FORM_ID>"
watch_id = "<YOUR_WATCH_ID>"

# Print JSON response after deleting a form watch
result = (
    service.forms().watches().delete(formId=form_id, watchId=watch_id).execute()
)
print(result)

Node.js

forms/snippets/delete_watch.js
import path from 'node:path';
import {authenticate} from '@google-cloud/local-auth';
import {forms} from '@googleapis/forms';

// TODO: Replace with a valid form ID.
const formID = '<YOUR_FORM_ID>';
// TODO: Replace with a valid watch ID.
const watchID = '<YOUR_FORMS_WATCH_ID>';

/**
 * Deletes a watch from a form.
 */
async function deleteWatch() {
  // Authenticate with Google and get an authorized client.
  const authClient = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/drive',
  });

  // Create a new Forms API client.
  const formsClient = forms({
    version: 'v1',
    auth: authClient,
  });

  // Send the request to delete the watch.
  const result = await formsClient.forms.watches.delete({
    formId: formID,
    watchId: watchID,
  });

  console.log(result.data);
  return result.data;
}

授权

与对 Forms API 的所有调用一样,对 watches.create() 的调用必须通过授权令牌进行授权。令牌必须包含一个范围,用于授予对要发送通知的数据的读取权限。

为了能够传送通知,应用必须保留已获授权用户的 OAuth 授权,且该授权具有所需的范围。如果用户断开应用连接,通知会停止,手表可能会因错误而暂停。 如需在重新获得授权后恢复通知,请参阅续订手表

列出表单的手表

Python

forms/snippets/list_watches.py
from apiclient import discovery
from httplib2 import Http
from oauth2client import client, file, tools

SCOPES = "https://www.googleapis.com/auth/drive"
DISCOVERY_DOC = "https://forms.googleapis.com/$discovery/rest?version=v1"

store = file.Storage("token.json")
creds = None
if not creds or creds.invalid:
  flow = client.flow_from_clientsecrets("client_secrets.json", SCOPES)
  creds = tools.run_flow(flow, store)
service = discovery.build(
    "forms",
    "v1",
    http=creds.authorize(Http()),
    discoveryServiceUrl=DISCOVERY_DOC,
    static_discovery=False,
)

form_id = "<YOUR_FORM_ID>"

# Print JSON list of form watches
result = service.forms().watches().list(formId=form_id).execute()
print(result)

Node.js

forms/snippets/list_watches.js
import path from 'node:path';
import {authenticate} from '@google-cloud/local-auth';
import {forms} from '@googleapis/forms';

// TODO: Replace with a valid form ID.
const formID = '<YOUR_FORM_ID>';

/**
 * Lists the watches for a given form.
 */
async function listWatches() {
  // Authenticate with Google and get an authorized client.
  const auth = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/forms.responses.readonly',
  });

  // Create a new Forms API client.
  const formsClient = forms({
    version: 'v1',
    auth,
  });

  // Get the list of watches for the form.
  const result = await formsClient.forms.watches.list({
    formId: formID,
  });

  console.log(result.data);
  return result.data;
}

续订手表

Python

forms/snippets/renew_watch.py
from apiclient import discovery
from httplib2 import Http
from oauth2client import client, file, tools

SCOPES = "https://www.googleapis.com/auth/drive"
DISCOVERY_DOC = "https://forms.googleapis.com/$discovery/rest?version=v1"

store = file.Storage("token.json")
creds = None
if not creds or creds.invalid:
  flow = client.flow_from_clientsecrets("client_secrets.json", SCOPES)
  creds = tools.run_flow(flow, store)
service = discovery.build(
    "forms",
    "v1",
    http=creds.authorize(Http()),
    discoveryServiceUrl=DISCOVERY_DOC,
    static_discovery=False,
)

form_id = "<YOUR_FORM_ID>"
watch_id = "<YOUR_WATCH_ID>"

# Print JSON response after renewing a form watch
result = (
    service.forms().watches().renew(formId=form_id, watchId=watch_id).execute()
)
print(result)

Node.js

forms/snippets/renew_watch.js
import path from 'node:path';
import {authenticate} from '@google-cloud/local-auth';
import {forms} from '@googleapis/forms';

// TODO: Replace with a valid form ID.
const formID = '<YOUR_FORM_ID>';
// TODO: Replace with a valid watch ID.
const watchID = '<YOUR_FORMS_WATCH_ID>';

/**
 * Renews a watch on a form.
 */
async function renewWatch() {
  // Authenticate with Google and get an authorized client.
  const authClient = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/drive',
  });

  // Create a new Forms API client.
  const formsClient = forms({
    version: 'v1',
    auth: authClient,
  });

  // Send the request to renew the watch.
  const result = await formsClient.forms.watches.renew({
    formId: formID,
    watchId: watchID,
  });

  console.log(result.data);
  return result.data;
}

限制

通知会受到限制 - 每只手表每 30 秒最多只能收到一条通知。此频次阈值可能会发生变化。

由于存在节流,单个通知可能对应多个事件。 换句话说,通知表示自上次通知以来发生了一个或多个事件。

限制

在任何时间,对于给定的表单和事件类型,每个 Cloud 控制台项目都可以有:

  • 最多可观看 20 次
  • 每位最终用户最多可获赠一块手表

此外,在任何时间,每个表单在所有 Cloud 控制台项目中,每个事件类型最多只能有 50 个观看者。

当手表使用最终用户的凭据创建或续订时,即与该最终用户相关联。如果关联的最终用户无法再访问相应表单或撤消了应用对相应表单的访问权限,则手表会被暂停。

可靠性

在所有情况下(特殊情况除外),每个手表在每次事件发生后至少会收到一次通知。在绝大多数情况下,通知会在事件发生后的几分钟内送达。

错误

如果手表的通知持续无法送达,手表状态会变为 SUSPENDED,并设置手表的 errorType 字段。如需将暂停的手表的状态重置为 ACTIVE 并恢复通知,请参阅续订手表

建议的用法

  • 使用单个 Cloud Pub/Sub 主题作为多个监控的目标。
  • 当收到有关某个主题的通知时,表单 ID 会包含在通知载荷中。将其与事件类型搭配使用,以了解要提取哪些数据以及要从哪种形式的数据中提取。
  • 如需在收到 EventType.RESPONSES 通知后提取更新的数据,请调用 forms.responses.list()
    • 将请求中的过滤条件设置为 timestamp > timestamp_of_the_last_response_you_fetched
  • 如需在收到 EventType.SCHEMA 通知后提取更新后的数据,请调用 forms.get()