トリガーを Web アプリの画面から登録すると起きる事故と、その防ぎ方
インストール型トリガーは、作った人のアカウントで動きます。 画面のボタンから作ると、その「作った人」が思っていた人と違うことがあります。先に何が起きるかを見てから、防ぎ方を組みます。
定期実行のある Web アプリでは、トリガーの登録をエディタで済ませてもらうことが多くなります。どの関数を選ぶのか、承認画面で何を押すのか、で止まる人もいます。登録を設定画面のスイッチにできれば、エディタを開いてもらう必要はなくなります。
仕組みとしては、画面のボタンから google.script.run でサーバーの関数を呼び、その中で ScriptApp.newTrigger() を呼べば、トリガーができます。手軽に作れるぶん、組み方を誤ると事故になります。
トリガーは、作った人のアカウントで動く
公式のガイドには、インストール型トリガーは常に作った人のアカウントで動く、と書かれています。トリガーから送るメールも、作った人の Gmail から、作った人の1日の枠で送られます。
Web アプリを「自分(Me)」として実行する設定でデプロイすると、誰が開いても、スクリプトはデプロイした人として動きます。Session.getEffectiveUser() も、この場合は開発者のアカウントを返します。この2つを合わせると、画面から作ったトリガーはデプロイした人のアカウントで動く形になります。
ただ、「Web アプリから作ったトリガーが誰のものになるか」を名指しした記述は、公式には見当たりません。以下は、この2つの記述から組み立てた見立てです。作ったあとに、デプロイした人のアカウントでエディタの「トリガー」の画面を開き、登録されているかを確かめておくと安心です。
画面から登録すると起きること
押した人ごとにトリガーが増える
デプロイを「アクセスしているユーザー」として実行する設定にしていると、スクリプトは開いた人の権限で動きます。スイッチを押した人それぞれのアカウントに、トリガーができる形になります。
3人が押せば、トリガーは3本になるはずです。毎朝のお知らせなら、3通届きます。
別のアカウントが入れたトリガーは見えない、とも公式のガイドに書かれています。ほかの人が作ったトリガーは、管理する人のエディタの「トリガー」の画面にも出てきません。どこに何本あるかを確かめられず、止めることもできなくなります。
デプロイし直すと、トリガーの持ち主が変わる
マニフェストの説明では、「自分」で実行する Web アプリは、デプロイした人として動きます。最初に設置した人とは別のアカウントでデプロイし直すと、そこから先に作るトリガーは、新しくデプロイした人のものになります。
前のアカウントで作ったトリガーは、新しいアカウントからは見えません。画面では「オフ」に見えるのでスイッチを入れ直すと、前のトリガーも動き続けたまま、2本目ができます。お知らせは2通ずつ届くようになり、どちらのアカウントから見ても、1本しかないように見えます。
ほかの管理者が、設置者の Gmail から送り始められる
スイッチを管理者なら誰でも押せるようにしていると、設置者ではない管理者がオンにした時点で、設置者の Gmail から毎朝メールが出始めます。減っていくのは設置者の1日の枠です。
設置者からすると、自分の名前で知らないうちにメールが出ていることになります。届いた人から問い合わせが来て、はじめて気づくこともあります。
ボタンを隠しても、関数は呼べる
スイッチを管理者の画面にだけ出しても、それだけでは止まりません。名前が _ で終わらないトップレベルの関数は、画面の側から呼べる状態になります。画面のソースを調べた人は、画面で使っていない関数の名前も見つけられます。
押せる人を絞るなら、サーバー側の関数の中で確かめる必要があります。
防ぎ方
切り替えられるのは、設置者だけにする
トリガーが設置者のアカウントで動き、設置者の Gmail から送るなら、オンにするかどうかは設置者が決めるのが筋です。ほかの管理者には、状態だけを見せます。
設置者かどうかは、最初に動いたときに控えた設置者のアドレスと、開いた人のアドレスを比べて見分けます。「自分」で実行する Web アプリでも、設置者本人のアドレスは取れます。見分け方は誰が開いているかを見分ける記事にまとめています。
スクリプトを動かしているアカウントを確かめる
トリガーを作る前に、Session.getEffectiveUser() のアドレスが設置者のアドレスと同じかを見ます。このメソッドは、スクリプトを動かしている権限の持ち主を返します。
「自分」で正しくデプロイされていれば、設置者のアドレスが返ってきます。「アクセスしているユーザー」で配られていたら、押した人のアドレスです。別のアカウントでデプロイし直されていたら、そのアカウントのアドレスです。どちらの場合も設置者とは違うので、そこで止めれば、設置者ではない人のトリガーはできません。
二重に作らない
登録の前に、同じ関数のトリガーがあるかを見ます。スイッチを2回押しても1本のままです。2つの画面からほぼ同時に押されたときに備えて、ロックで1回ずつにします。重複を避ける考え方は、時間主導型トリガーの記事でも扱っています。
コードにすると
3つの守りを入れた、サーバー側の関数です。
const HANDLER = 'sendDailyDigest'; // 毎日動かしたい関数の名前
// 名前が _ で終わる関数は、画面から呼べない
function findTrigger_() {
return ScriptApp.getProjectTriggers().find((t) => t.getHandlerFunction() === HANDLER) || null;
}
// スクリプトを動かしているアカウントが、控えておいた設置者のアカウントと同じか
function runsAsOwner_() {
const owner = PropertiesService.getScriptProperties().getProperty('ownerEmail') || '';
const effective = Session.getEffectiveUser().getEmail() || '';
return owner !== '' && effective.toLowerCase() === owner.toLowerCase();
}
function getDigestStatus() {
return { on: findTrigger_() !== null, canChange: isOwner_() };
}
function setDigest(on) {
if (!isOwner_()) throw new Error('切り替えられるのは、設置した人だけです');
if (!runsAsOwner_()) throw new Error('デプロイの「実行するユーザー」を「自分」にしてから切り替えてください');
const lock = LockService.getScriptLock();
lock.waitLock(10000); // 2つの画面からほぼ同時に押されたときに、2本作らないため
try {
const current = findTrigger_();
if (on && !current) ScriptApp.newTrigger(HANDLER).timeBased().atHour(8).everyDays(1).create();
if (!on && current) ScriptApp.deleteTrigger(current);
return { on: findTrigger_() !== null };
} finally {
lock.releaseLock();
}
}
isOwner_() は、開いた人が設置者かどうかを返す自分の関数に置き換えてください。ownerEmail は、最初に動いたときに Session.getEffectiveUser() のアドレスを控えておいたものです。正しい設定で最初に開いたときに控えておけば、あとでデプロイの設定が変わったときに気づけます。
状態は、トリガーから読む
スイッチの入り切りを、別にシートやプロパティへ保存しておく必要はありません。ScriptApp.getProjectTriggers() は、今のユーザーがこのプロジェクトに作ったトリガーを返します。「自分」で実行していれば、デプロイした人のトリガーが返ってくるはずです。その中に目当ての関数があるかどうかが、そのままスイッチの状態になります。
状態を別に持つと、エディタの画面からトリガーを消されたときに、表示と実際が食い違います。上の setDigest() も、最後にトリガーを読み直した結果を返しています。
画面の側は、設定を開いたときに getDigestStatus() を呼びます。canChange が false の人には、スイッチの代わりに今の状態だけを出します。
toggle.disabled = !status.canChange; // 設置者でなければ押せない
toggle.addEventListener('change', () => {
toggle.disabled = true; // 返事が来るまで、続けて切り替えられないようにする
google.script.run
.withSuccessHandler((res) => {
toggle.checked = res.on;
toggle.disabled = false;
})
.withFailureHandler((err) => {
toggle.checked = !toggle.checked; // 押す前の状態に戻す
toggle.disabled = false;
showError(err.message);
})
.setDigest(toggle.checked);
});
status は getDigestStatus() の戻り値、toggle はスイッチの <input type="checkbox">、showError() はエラーを出す自分の関数に置き換えてください。画面で押せなくしても、サーバー側の確かめは外さないでください。
防いだあとも知っておくこと
時刻はずれ、動く時間には上限がある
atHour(8) で登録しても、8時ちょうどには動かず、8時台のどこかで動きます。時刻はスクリプトのタイムゾーンで決まります。プロジェクトの設定のタイムゾーンが日本の時刻になっているかも見ておいてください。
| 上限 | 個人のアカウント | Workspace |
|---|---|---|
| トリガーの数 | 20個 | 20個 |
| トリガーで動く合計 | 1日90分 | 1日6時間 |
| 1回の実行 | 6分 | 6分 |
トリガーの数は、1つのスクリプトにつき、ユーザーごとに数えます。毎朝1回、数秒で終わる処理なら、どの上限にも近づきません。
失敗は、画面ではなくメールで届く
トリガーで動いた関数が失敗しても、画面には何も出ません。そのかわり、Apps Script から失敗をまとめたメールが送られます(差出人は noreply-apps-scripts-notifications@google.com)。メールの中には、トリガーを止めたり設定し直したりするリンクが入っています。
あとから足すときは、承認をやり直す
すでに配っているアプリに、あとからトリガーの登録を足すことがあります。トリガーを扱うには、script.scriptapp というスコープの権限が要ります。
コードの変更で新しいサービスが加わると、承認済みのスクリプトでも追加の承認が要ります。トリガーは、作った人が承認していないと動きません。新しい権限が要るコードを足したら、エディタで関数を一度実行して承認画面を出すよう、公式のガイドでは案内されています。この1回だけは、設置者にエディタでの承認をお願いすることになります。
書かなくても動く状態のコードを、下のツールで配っています。
社内wiki手順書や議事録を1か所にまとめます。契約も月額もなく、データは自分の Google ドライブの中だけに置かれます。