设置和接收推送通知

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

概览

Google 表单 API 推送通知功能可让应用在表单数据发生更改时订阅通知。通知会传送至某个 Cloud Pub/Sub 主题,通常在发生更改后的几分钟内完成。

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

以下是本文档中所用关键概念的定义:

  • 目标是发送通知的位置。唯一支持的目标是 Cloud Pub/Sub 主题。
  • 事件类型是第三方应用可订阅的通知类别。
  • 手表是 Form API 的一项指令,旨在将特定表单上的特定事件类型的通知传送给目标。

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

您的 Cloud Pub/Sub 主题仅接收关于您可以使用自己提供的凭据查看的表单的通知。例如,如果用户撤消了您的应用的权限,或失去了对监控表单的编辑权限,系统将不再传送通知。

可用的事件类型

Google Form 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 价格,并为您的 Play 管理中心项目启用结算功能。
  4. 通过以下三种方式之一创建 Cloud Pub/Sub 主题:

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

  6. 最后,在创建以您的主题为目标的手表之前,您需要为表单通知服务帐号 (forms-notifications@system.gserviceaccount.com) 授予权限,以便发布到您的主题。

创建手表

有了表单 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

form/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;

授权

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

若要传送通知,应用必须保留经授权的用户提供的具有所需范围的 OAuth 授权。如果用户断开应用连接,通知会停止,并且手表可能会因为错误而被暂停。如需在重新获得授权后恢复通知功能,请参阅为手表续期

列出表单的手表

Python

form/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

form/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

form/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()