はじめに

BOOTHで販売しているアセットに関するフィードバックを収集するためにGoogle Formsを使ったフォームを設置しています。

今まではメールで回答の通知を設定していたのですが、メールを見落とすことがあるため回答内容をDiscordに通知する仕組みを作りました。

この記事ではその紹介とスクリプトの詳細を記載します。

作ったもの

今回は以下のフォームに通知を設定していきます。

フォームの内容

上記のフォームに回答すると、以下のようなメッセージがDiscordに通知されます。

Discordに通知されるメッセージ

スクリプトと設定

今回作成したスクリプトは以下のようになりました。

script.gs

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
function onFormSubmit(event) {
  processResponse(event.response);
}

function processResponse(response) {
  const payload = buildDiscordMessage(response);
  const endpoint = getSecret("webhookEndpoint");
  const postFunc = buildPostToDiscordFunc(endpoint);
  postFunc(payload);
}

function buildDiscordMessage(response) {
  const form = FormApp.getActiveForm();

  const kind = buildFetchElementResponseFunc(123456780)(response);
  const productId = buildFetchElementResponseFunc(123456781)(response);
  const content = buildFetchElementResponseFunc(123456782)(response);

  const embed = {
    "description": `${form.getTitle()} に回答があります`,
    "fields": [
      {
        "name": "種別",
        "value": kind,
        "inline": true,
      },
      {
        "name": "URL",
        "value": buildBoothProductUrl(productId),
        "inline": true,
      },
      {
        "name": "内容",
        "value": content,
        "inline": false,
      },
    ],
  };
  switch (kind) {
    case "要望":
      embed["color"] = 0x609dff;
      break;

    case "不具合報告":
      embed["color"] = 0xff7777;
      break;
  }

  return {
    "embeds": [embed],
  };
}

function buildBoothProductUrl(id) {
  if (/^\d+$/.test(id)) {
    return `https://booth.pm/ja/items/${id}`;
  }
  return "";
}

function buildPostToDiscordFunc(endpoint) {
  return function(payload) {
    return UrlFetchApp.fetch(
      endpoint,
      {
        "method": "POST",
        "contentType": "application/json",
        "payload": JSON.stringify(payload),
      },
    );
  };
}

// === GAS/Form functions ===

function buildFetchElementResponseFunc(elementId) {
  return function(response) {
    for (let item of response.getItemResponses()) {
      if (item.getItem().getId() == elementId) {
        return item.getResponse();
      }
    }
    throw new Error(`Form element whose id is ${elementId} not found`);
  }
}

function getSecret(key) {
  return PropertiesService.getScriptProperties().getProperty(key);
}

// === Debug functions ===

// Dumps form elements with their id
function dumpFormElements() {
  const form = FormApp.getActiveForm();
  const items = form.getItems().map(item => `id: ${item.getId()}, title: ${item.getTitle()}`);
  console.info(items.join("\n"));
}

// Processes first response to test
function testSendFirstResponse() {
  const form = FormApp.getActiveForm();
  const responses = form.getResponses();
  if (responses.length == 0) {
    console.warn("No response recorded")
    return;
  }
  processResponse(responses[0]);
}

上記のスクリプトをGoogleフォームのスクリプトエディタに貼り付け、Discordのサーバー設定からWebhookを作成し、そのURLを取得しておきます。

このWebhookのURLは秘匿しておくためにGASのスクリプトプロパティに保存します。

スクリプトプロパティの設定

最後に、以下のようにトリガーを設定します。

トリガーの設定

スクリプトの詳細

エントリーポイント

onFormSubmitをフォーム回答時に実行されるトリガーとして設定し、フォームの回答1件に対してprocessResponseを呼び出しています。

processResponseではbuildDiscordMessageでフォームの回答内容をDiscordに投稿するメッセージに変換し、buildPostToDiscordFuncでWebhookURLから作成した送信用の関数にメッセージを渡しています。

メッセージの構築

buildDiscordMessageではFormResponseオブジェクトから回答内容をEmbedに変換した上でペイロードとして返します。

フォームの回答内容はFormResponseオブジェクトとして取得できるので、必要な情報はこのオブジェクトを経由して取り出します。

ここでは要望と不具合報告の種別によってEmbedの色を変え、必要な回答項目を埋め込んでいます。

また回答項目は文言が変化してもスクリプトの修正が不要となるように、一意のIDによって取得するようにしています。このIDは後述するdumpFormElementsで取得します。

デバッグ、補助関数

dumpFormElementsはフォームの項目のIDと名前を取得してコンソールに出力します。

実際に実行すると、以下のような出力が得られます。

id: 123456780, title: フィードバック種別
id: 123456781, title: 商品ID (自動入力されるため編集不要です)
id: 123456782, title: 内容

この結果から、buildFetchElementResponseFuncで取得する回答項目のIDを特定します。

testSendFirstResponseはフォームの一番最初の回答を引数としてprocessResponseを呼び出します。 回答のトリガーのみでテストすると何回も回答を入力する必要があるため、繰り返し動作を確認する際に利用しています。

おわりに

Google App Scriptを使ってGoogle Formsの回答をDiscordに通知する仕組みを作りました。 この仕組みにより、フォームの回答の見逃がしを防ぐことができそうです。