設定及接收推播通知

您可以使用「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 通知,或輪詢訂閱端點。

如要設定 Cloud Pub/Sub 主題,請按照下列步驟操作:

  1. 完成 Cloud Pub/Sub 先決條件
  2. 設定 Cloud Pub/Sub 用戶端
  3. 查看 Cloud Pub/Sub 價格,並為 Developer Console 專案啟用帳單。
  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 Console 專案可有:

  • 最多可觀看 20 次
  • 每位使用者最多可配對一支手錶

此外,在所有 Cloud Console 專案中,每個表單的事件類型總共只能有 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()