设置和接收推送通知

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

概览

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

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

下面列出了本文档中使用的关键概念的定义:

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

为特定表单上的事件类型创建监控后,该监控的目标(即 Cloud Pub/Sub 主题)会收到来自该表单上的这些事件的通知,直到监控到期为止。手表的有效期为一周,但您可以在其到期之前的任何时间通过向 watches.renew() 发出请求来延长其有效期。

您的 Cloud Pub/Sub 主题仅会收到有关您可以使用您提供的凭据查看的表单的通知。例如,如果用户撤消了对您应用的权限或失去了对所关注表单的修改权限,系统便不会再传送通知。

可用的事件类型

Google 表单 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 在 webhook 上接收通知,也可以通过轮询订阅端点接收通知。

如需设置 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
'use strict';

const path = require('path');
const google = require('@googleapis/forms');
const {authenticate} = require('@google-cloud/local-auth');

const formID = '<YOUR_FORM_ID>';

async function runSample(query) {
  const authClient = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/drive',
  });
  const forms = google.forms({
    version: 'v1',
    auth: authClient,
  });
  const watchRequest = {
    watch: {
      target: {
        topic: {
          topicName: 'projects/<YOUR_TOPIC_PATH>',
        },
      },
      eventType: 'RESPONSES',
    },
  };
  const res = await forms.forms.watches.create({
    formId: formID,
    requestBody: watchRequest,
  });
  console.log(res.data);
  return res.data;
}

if (module === require.main) {
  runSample().catch(console.error);
}
module.exports = runSample;

删除手表

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
'use strict';

const path = require('path');
const google = require('@googleapis/forms');
const {authenticate} = require('@google-cloud/local-auth');

const formID = '<YOUR_FORM_ID>';
const watchID = '<YOUR_FORMS_WATCH_ID>';

async function runSample(query) {
  const authClient = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/drive',
  });
  const forms = google.forms({
    version: 'v1',
    auth: authClient,
  });
  const res = await forms.forms.watches.delete({
    formId: formID,
    watchId: watchID,
  });
  console.log(res.data);
  return res.data;
}

if (module === require.main) {
  runSample().catch(console.error);
}
module.exports = runSample;

授权

与对 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
'use strict';

const path = require('path');
const google = require('@googleapis/forms');
const {authenticate} = require('@google-cloud/local-auth');

const formID = '<YOUR_FORM_ID>';

async function runSample(query) {
  const auth = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/forms.responses.readonly',
  });
  const forms = google.forms({
    version: 'v1',
    auth: auth,
  });
  const res = await forms.forms.watches.list({formId: formID});
  console.log(res.data);
  return res.data;
}

if (module === require.main) {
  runSample().catch(console.error);
}
module.exports = runSample;

续订手表

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
'use strict';

const path = require('path');
const google = require('@googleapis/forms');
const {authenticate} = require('@google-cloud/local-auth');

const formID = '<YOUR_FORM_ID>';
const watchID = '<YOUR_FORMS_WATCH_ID>';

async function runSample(query) {
  const authClient = await authenticate({
    keyfilePath: path.join(__dirname, 'credentials.json'),
    scopes: 'https://www.googleapis.com/auth/drive',
  });
  const forms = google.forms({
    version: 'v1',
    auth: authClient,
  });
  const res = await forms.forms.watches.renew({
    formId: formID,
    watchId: watchID,
  });
  console.log(res.data);
  return res.data;
}

if (module === require.main) {
  runSample().catch(console.error);
}
module.exports = runSample;

限制

系统会限制通知的数量,每部手表每 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()