設定及接收推播通知

您可以使用「Watches」集合中的方法,在表單資料變更時接收通知。本頁提供設定及接收推播通知的概念總覽和操作說明。

總覽

Google form API 推播通知功能可讓應用程式訂閱表單資料變更時的通知。通知通常會在變更後的幾分鐘內傳送至 Cloud Pub/Sub 主題。

如要接收推播通知,您必須設定 Cloud Pub/Sub 主題,並在針對適當的事件類型建立手錶時提供主題名稱。

以下是本文件使用的重要概念定義:

  • 「目標」是傳送通知的地方。唯一支援的目標是 Cloud Pub/Sub 主題。
  • 「事件類型」是第三方應用程式可以訂閱的通知類別。
  • 「手錶」是表單 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 中,您可以透過網路掛鉤或輪詢訂閱端點接收通知。

如要設定 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. 最後,在建立指定您主題的手錶之前,您必須授予表單通知服務帳戶 (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;

授權

與對表單 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;

調節

通知受到限制 (每三十秒最多只會收到一次通知)。此頻率上限可能隨時變動。

由於節流機制,一則通知可能對應至多個事件。換句話說,通知指出自上次通知起已發生一或多個事件。

限制

不論何時,對於特定表單和事件類型,每個 Cloud Console 專案都可以擁有:

  • 最多共 20 次觀看
  • 每位使用者最多 1 支手錶

此外,在所有 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()