GASからDiscordにWebhookでメッセージ通知を送信する方法

GAS
目次

Google スプレッドシートの集計結果やバッチ処理の完了ステータスを外部チャットに飛ばしたいとき、Discordは非常に有力な通知先になります。Slackの無料プランにおけるメッセージ履歴保持期間が短縮されたこともあり、社内開発チームの監視ログや個人の副業案件、コミュニティ運営のダッシュボード通知先としてDiscordのテキストチャンネルを選択するケースが増えています。Google Apps Script(GAS)からDiscordへの通知はWebhookのURLさえあれば簡単に実行できるように見えますが、実際に組み込んでみるとHTTPリクエストのヘッダー設定ミスで400 Bad Requestが出たり、文字列の組み立てミスで429レートリミットに引っかかったりと、細かな落とし穴が存在します。本稿では、GASからDiscordへWebhook経由で確実に通知を送信するための設計と、現場で踏みがちな不具合の回避策をまとめます。

UrlFetchApp.fetchで嵌まりがちなペイロード形式とヘッダーの落とし穴

GASから外部のHTTPエンドポイントに対してデータを送信する場合、標準の通信手段としてUrlFetchApp.fetchメソッドを使用します。DiscordのWebhookへメッセージを送信する処理において、最も初心者が躓きやすく、かつ経験者でも急いで書いていると見落としがちなのが、送信するリクエストボディのシリアライズとコンテントタイプの指定です。

GASのUrlFetchApp.fetch(url, params)において、第2引数のオプションオブジェクト内にpayloadを指定できます。フォーム送信など一般的なWebリクエストであれば、キーとバリューのオブジェクトをそのままpayloadに渡すと、GAS側で自動的にapplication/x-www-form-urlencodedやマルチパート形式として展開されて送信されます。しかし、DiscordのWebhook APIエンドポイント(https://discord.com/api/webhooks/...)は、リクエストボディとしてJSON形式(application/json)を厳格に要求します。

もしpayloadにJavaScriptのオブジェクトをそのまま渡したり、contentTypeプロパティを省略した状態で送信を実行すると、Discordのサーバー側はリクエストを正しくパースできず、即座に400 Bad Request(あるいは中身を読み取れない旨のエラー)を返答します。しかも、GASのデフォルトの挙動では、HTTPレスポンスのステータスコードが400番台や500番台のとき、スクリプトの実行そのものが例外を投げて異常終了してしまいます。このため、スプレッドシート側で後続の更新処理やセルの色変えなどを行っていた場合、それらの処理がすべて巻き込まれて停止するという事態に陥ります。

この問題を回避するための必須要件は2点あります。1点目は、送信したいJavaScriptオブジェクトを必ずJSON.stringify(payload)によってプレーンなJSON文字列へと変換してからpayloadに代入することです。2点目は、オプションオブジェクトのキーとしてcontentType: "application/json"を明示的に宣言することです。この2つの手続きが揃って初めて、DiscordのAPIサーバーは受け取ったデータを正規のメッセージ作成要求として解釈します。GASで外部連携を書く際には、内部オブジェクトと外部に投げるデータ形式の境界を意識し、文字列への変換ステップを漏らさない設計が不可欠です。

Webhook経由でメッセージを安全に送信する最小構成の処理

実際にGASからDiscordへメッセージを自動投稿する最小限のスクリプト構成を見ていきます。この処理は、スプレッドシートの夜間トリガー実行完了時や、Googleフォーム送信時にテキストチャンネルへ速報を流す用途で頻繁に利用します。

以下のスクリプトは、指定したWebhookエンドポイントに向けてテキストメッセージをPOST送信する最も標準的な記述です。実務運用を見据え、スクリプト実行がエラーでクラッシュしないようmuteHttpExceptionsオプションも併せて設定しています。

function sendDiscordNotification() {
  const discordUrl = "https://discord.com/api/webhooks/xxx/yyy";
  const payload = {
    content: "📢 本日の自動レポート出力が完了しました。"
  };

  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(discordUrl, options);
  const responseCode = response.getResponseCode();

  if (responseCode !== 200 && responseCode !== 204) {
    console.error("Discord通知に失敗しました。ステータスコード: " + responseCode + " 内容: " + response.getContentText());
  } else {
    console.log("Discord通知が正常に完了しました。");
  }
}

このコードをいつ使うかというと、バッチ処理の終了合図や異常検知の監視アラートをチームへ一斉配信したい局面です。メール通知に比べてリアルタイム性が高く、関係者全員が同じタイムラインでインシデントや完了報告を確認できます。

ここで実務上ずれる点として挙げられるのが、Discord側の仕様における「メッセージ文字数の制限」と「空文字送信の禁止」です。Discordのメッセージ本文を格納するcontentフィールドには、1メッセージあたり最大2,000文字という上限が存在します。スプレッドシート内の複数行のログデータやエラーのスタックトレースをまとめて結合し、1つの変数に入れて送信しようとした場合、2,000文字を超えた瞬間にDiscord APIから400 Bad Requestが返され、通知が届かなくなります。また、スプレッドシートの特定セルが空欄だった場合に、content: ""のように空文字やnull、未定義値をそのまま渡してしまうと、送信内容が存在しないと判定されて同様にエラーとなります。

この不具合を回避するためには、送信直前に文字列長をチェックし、2,000文字を超える場合はsubstringなどで分割送信するか末尾を切り詰める処理を挟む必要があります。また、メッセージ変数が空である可能性を考慮し、デフォルトメッセージ(例:「ログなし」など)をフォールバックとして設定しておくことが、予期せぬスクリプト停止を防ぐための強固な防壁になります。

embedsオブジェクトによるリッチ表示と構造化データの送信

単なる平文のcontentだけでなく、Discord連携の真価を発揮するのがembedsオブジェクトを用いたリッチなカード形式の通知です。Discordコミュニティや個人サーバーへの通知基盤として手軽に運用できる利点を最大限に活かすには、見た目にも情報が整理された埋め込み表示を活用するのが効果的です。

埋め込み表示を行う場合、payloadのトップレベルにembedsという名前の配列プロパティを配置します。この配列の中に、タイトル、説明文、カラーコード、任意のフィールド群などを設定したオブジェクトを格納します。カラーバーの色によって「正常終了(緑色)」「注意(黄色)」「致命的エラー(赤色)」といった視覚的な優先度を一目で判断できるようになるため、監視通知の運用効率が劇的に向上します。

ただし、このembedsの構造をGASで組み立てる際にも、いくつか注意すべき仕様のズレがあります。特に注意が必要なのがcolorプロパティの型指定です。CSSやWeb制作で一般的なカラーコードは#00ff00や#ff0000といった16進数文字列ですが、DiscordのAPIが要求するcolorは16進数を10進数に変換した「数値型(Integer)」です。たとえば緑色を示す#00FF00を指定したい場合は、0x00FF00(10進数で65280)という数値を渡す必要があります。もしここに"#00ff00"という文字列型を渡してしまうと、ペイロードのバリデーションに失敗してリクエスト全体が弾かれます。

さらに、fields配列を使ってスプレッドシートの集計項目(「処理件数: 100件」「エラー件数: 0件」「実行時間: 1.2秒」など)を横並びで表示する際、1つの埋め込みカードの中に格納できるフィールド数には25個という上限があります。各フィールドのnameは最大256文字、valueは最大1,024文字という厳密な文字数制限も設けられています。スプレッドシートの行データを動的にループしてfieldsへ機械的に突っ込むようなコードを書いていると、処理対象データが多い日だけ突然上限を超過し、APIエラーで通知が沈黙するという障害を引き起こします。リッチ通知を組む際は、必ず最大件数を制限し、超過分はスプレッドシート自体のリンクをdescriptionに掲載して参照させる運用が安全です。

レートリミット(429)と例外停止を防ぐfetchの運用設計

GASからDiscordへWebhookを送信する運用が拡大するにつれて、必ず直面するのがレートリミット(短時間の過剰リクエストによる制限)の壁です。DiscordのWebhookエンドポイントにはチャンネルごとにリクエスト頻度の制限が設けられており、短時間に何十件ものメッセージを集中して送信すると、HTTPステータスコード429 Too Many Requestsが返却されます。

スプレッドシート上でループ処理を回しながら「1行処理するごとにDiscordへログを送信する」といった実装をしてしまうと、十数件目あたりで即座にレートリミットに達します。前述の通り、GASのUrlFetchApp.fetchは標準状態で4xx系エラーを受け取ると例外を投げてプロセスを落としてしまうため、ループ処理の途中でスクリプト全体がクラッシュし、スプレッドシートのデータ更新が中途半端な状態で中断される最悪の事態を招きます。

このレートリミット問題に対処するための第一歩は、先述のコード例にも含めたmuteHttpExceptions: trueの指定です。このオプションをUrlFetchApp.fetchのパラメータに渡すことで、Discord側から429や500などのエラーが返ってきた場合でもGASが例外を投げず、正常にレスポンスオブジェクトを受け取れるようになります。レスポンスを受け取った上で、response.getResponseCode()を検査し、ステータスが429であればUtilities.sleep(1000)などで一時待機してから再試行する、あるいはエラーを配列に退避させて最後にまとめてログ出力する、といった安全な例外処理が書けるようになります。

加えて、アーキテクチャの観点からは「ループ内でWebhookを叩かない」ことが鉄則です。1行ごとの進捗をリアルタイムに投稿するのではなく、スプレッドシートの処理中はメモリ内の配列にメッセージを蓄積しておき、すべての処理が完了した段階で1通のサマリーメッセージ、あるいは数通のブロックにまとめて送信するバッチ通知の構成を採るべきです。これによりDiscord側のAPI負荷を劇的に抑えられるだけでなく、GASの1回あたりの実行時間上限(無料アカウントで6分)の消費を大幅に削減できます。

認証情報のスクリプトプロパティ管理と運用の安全性確保

最後に、DiscordのWebhook URL自体の管理方法について触れておかなければなりません。Webhook URLには、エンドポイントのパス部分に固有のトークンが含まれており、このURLを知っている人物であれば誰でも認証なしで指定チャンネルに任意のメッセージを投稿できてしまいます。

スプレッドシートのコンテナバインドスクリプトとしてコードを書いている場合、コード内に直接Webhook URLを文字列リテラルとしてハードコードしてしまうと、そのスプレッドシートを共有された第三者全員にWebhookの送信権限が漏洩することになります。万が一URLが流出した場合、チャンネルにスパムが投稿されたり、悪意のあるリンクが貼られたりするリスクが生じます。

この危険を排除するためには、Webhook URLをスクリプト内に直接書かず、GASの「スクリプトプロパティ(PropertiesService)」を活用して環境変数のように外部化します。スクリプトプロパティにDISCORD_WEBHOOK_URLというキーでURLを保存しておき、コード内ではPropertiesService.getScriptProperties().getProperty("DISCORD_WEBHOOK_URL")として呼び出す形を徹底します。この運用を徹底することで、コードをGit等でバージョン管理したり他人に見せたりする際にも秘匿情報が漏れず、万が一URLを変更・再生成することになった場合でも、コードを1行も書き換えることなくプロパティの更新だけで即座に対応できるようになります。

GASとDiscordの連携は、UrlFetchAppのオプション指定、データ型の変換ルール、そしてレートリミットへの配慮という要所をしっかり押さえておくことで、極めて安定した業務通知パイプラインとして機能します。シンプルな仕組みだからこそ、エラーハンドリングと安全管理の基本を疎かにせず、堅牢なスクリプトとして構築してください。

タイトルとURLをコピーしました