GASでQRコード受付システムが出来た〜〜〜!
2022/05/08追記
今更ながらgithubにコード上げました。
https://github.com/ymgcmnk/GAS-QR
追記おわり。
以下原文。
頑張った。頑張ったぞ。
分からなすぎて、泣いてたよ。
理解の遅いアホなりに頑張ったよ。
こんなんあったらいいな〜、と思ったものが、こうして形になってめちゃくちゃ嬉しい!!
まだまだ荒っぽいところもあるかと思いますが、ひとまず動いたのが嬉しいよ~~~!!!
コードはあんまり書けなかったので、noteに書くことで貢献(?)できればと思います。
もっとこうしたほうが良いよ!などありましたら、お気軽にコメントください~。
このネタで12/14のノンプロ研講座「GAS中級講座第7期卒業LT大会」で話そうかな~と思ってますー。
勢いで書いていて乱文、読みにくくてごめん!1万字超えてもうた…コード込みだから…たぶん…
免責 Disclaimer
このコードを用いて、ミスったり不具合があったりしても責任を負えませんので悪しからずご了承くださいませ。
趣味のイベントとか、小規模な集まりにおいては、それなりに動くと思います、が、厳密な運用管理が必要なものは、ちゃんとしたシステム使ってください。
使うなら使うで、ちゃんとテストしてくださいね。
どうなっても、わしゃ知らんよ!
運用上の諸注意や、GASの割り当て・使用制限もよく読んでね。
本ツールの前段階
抽選番号、受付番号をお知らせするツールを作っていた。
ここから大きく進化したもんだ......しゅごい。
やりたいこと
フォームで申込みを受け付けて、受け付けましたメールにQRコードを付けて、そのQRコードを受付で読み込んだら受付日時をシートに反映したい。
ざっくり作り方手順
詳細は後述。この通りの順番でなくてもいいんですが、ま、目安として。
1.フォームを作成
2.スプレッドシートを作成
3.スプレッドシートのコンテナバインドでスクリプト作成
4.doGet デプロイ
5.URLをスクリプトに反映
6.トリガーの設定
7.できた
1.フォームを作成
なんでもいいんですが、例えばこんな感じで、まずはフォームを作成。
自分はデフォルトでメールアドレスを収集することが多いです。
2.スプレッドシートを作成
フォームの回答となるシートを作成します。フォームの回答から、スプレッドシートを作成。
で、ここから先は好みの問題もあるかもしれませんが、今回は回答の生データとなる「フォームの回答 1」のシートはそのままとして、出席管理のシートとdoGetのシートを作ることにします。
3.スプレッドシートのコンテナバインドでスクリプト作成
フォームのコンテナバインドスクリプトにすると、イベントオブジェクトを取得できないので、スプレッドシートのコンテナバインドでスクリプト(コード)を書いてください。
コンテナバインドスクリプトってなんだ?って方は、
↓こちらの「いつも隣にITのお仕事(通称 隣IT)」のブログ記事を熟読してください。
コードは、メインの処理とdoGetで分けて書きます。
メインコード
ひとつのスクリプトファイルの中にこれら5つの関数と、プラスアルファでテスト関数やプロパティストア用の関数を書いてます。
メインとなる5つの関数一式のコードはこちら。
/**
* フォーム回答時にトリガ発火。
* @param {object} e イベントオブジェクト
* @return {Array} フォームの回答とIDの配列
*/
function retrieveFormAnswer(e) {
const sheet = e.source.getActiveSheet();
console.log(e);
console.log(sheet.getName());
const ss = SpreadsheetApp.getActiveSpreadsheet();
// const originalSHT = ss.getSheetByName('フォームの回答 1');
const answerSHT = ss.getSheetByName('出席管理');
//IDを作成して、イベントオブジェクトに追加
const today = Utilities.formatDate(new Date(), 'JST', 'yyyyMMdd');
e.id = idCreator(e, 3, today);
//受付用URLを生成して、イベントオブジェクトに追加
e.receptionUrl = createReceptionUrl(e);
//QRBlobを生成して、イベントオブジェクトに追加
e.qrcode = createQrBlob(e.receptionUrl);
//メールを送信
sendEmail(e);
//IDをフォームの回答 1 スプレッドシートに記録
// e.range.getCell(1, 1).offset(0, -1).setValue(e.id); //A列に記録
//NOTE: 範囲→単一セルを選択する方法 Range.getCell(相対行,相対列) 範囲外の場合はoffset()
//NOTE: 同じく Range.getNextDataCell(SpreadsheetApp.Direction.PREVIOUS) でも動く、けど、getCellのほうが多分動作が軽い(スプシ読む必要無いので)
//NOTE: https://caymezon.com/gas-cell-range/#toc2
//出席管理 スプレッドシートに記録 転記
const array = e.values;
array.push(e.id);
answerSHT.appendRow(array);
SpreadsheetApp.flush();
}
/**
* idCreator ファンクション:新規データのIDを作成する(e.range.rowStartでもいいけど、ちょっと遊びを)
* @param {object} e フォーム回答イベントのイベントオブジェクト
* @param {number} len IDの長さ 初期値3、1→上限26個、2→上限676個、3→上限17,576個、4→上限456,976個、5→上限11,881,376個
* @param {string} prefix IDの接頭文字を指定する
* @param {string} surfix IDの接尾文字を指定
* @return {string} id 完成したID
*/
function idCreator(e, len = 3, prefix = "", surfix = "") {//仮引数prefix は、「デフォルト値」。e.id = idCreator(e, 3, today)でtodayを渡されると prefixはtodayになります。
const count = e.range.rowStart - 1;
//ID上限チェック
if (count > 26 ** len) throw `count[${count}]は、IDの長さ[${len}]で作成可能な上限[${26 ** len}]を超えています。`;
let id = "";
for (let i = len; i > 0; i--) {
id += String.fromCharCode(Math.floor((count - 1) % (26 ** i) / 26 ** (i - 1)) + 65); //65=A Aから始まるように
}
id = prefix + id + surfix;
return id;
}
/**
* createQrBlob イベントオブジェクトから、QRコードを生成してBLOBを返す
* @param {object} e イベントオブジェクト
* @return {blob} QRコードのBlob
*/
function createQrBlob(data) {
const qrCreateURL = `https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=${encodeURI(data)}`;
const option = {
method: 'get',
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(qrCreateURL, option);
return response.getBlob().setName('QRコード.png');
}
/**
* フォームの回答から、受付用URLを生成する
* @param {object} e イベントオブジェクト
* @return {string} url 受付用URL
*/
function createReceptionUrl(e) {
const baseUrl = PropertiesService.getScriptProperties().getProperty('RECEPTION_BASEURL');
return `${baseUrl}?id=${e.id}`;
}
/**
* イベントオブジェクトからメールを送信する
* @param {object} e イベントオブジェクト
*/
function sendEmail(e) {
const refUrl = 'https://www......';
//HTMLメールが表示出来ない場合の代替本文を作成
const bodyTemplate = `
${e.namedValues['氏名']} 様
この度は 受付フォーム にご回答いただきありがとうございます。
あなたの受付番号は「${e.id}」です。
当日のイベントについては、こちらのURLをご覧ください:
${refUrl}
添付ファイルのQRコードを受付でご提示下さい。
会社名 xxxx
イベントurl xxxx
メールアドレス xxxx
`;
const recipient = e.namedValues['メールアドレス']
const subject = 'xxxに登録されました';
const inlineImageCode = "<strong>イベント当日は、このQRコードを受付に提示してください。<strong><br><img src='cid:inlineImg'>";
const htmlBody = bodyTemplate.replace(/[\r\n]/g, '<br>')
.replace('添付ファイルのQRコードを受付でご提示下さい。', inlineImageCode);//htmlに置き換え
const options = {
htmlBody: htmlBody,
inlineImages: {
inlineImg: e.qrcode
},
attachments: e.qrcode
};
// ランダムでスリープかける。同時申込でメール送信できない現象回避のため。
const max = 30;
const min = 0;
const staySecond = Math.floor(Math.random() * (max - min + 1) + min);
Utilities.sleep(staySecond * 1000);
//メール送信
GmailApp.sendEmail(recipient, subject, bodyTemplate, options)
}
以下、各関数について。
retrieveFormAnswer
/**
* フォーム回答時にトリガ発火。
* @param {object} e イベントオブジェクト
* @return {Array} フォームの回答とIDの配列
*/
function retrieveFormAnswer(e) {
const sheet = e.source.getActiveSheet();
console.log(e);
console.log(sheet.getName());
const ss = SpreadsheetApp.getActiveSpreadsheet();
// const originalSHT = ss.getSheetByName('フォームの回答 1');
const answerSHT = ss.getSheetByName('出席管理');
//IDを作成して、イベントオブジェクトに追加
const today = Utilities.formatDate(new Date(), 'JST', 'yyyyMMdd');
e.id = idCreator(e, 3, today);
//受付用URLを生成して、イベントオブジェクトに追加
e.receptionUrl = createReceptionUrl(e);
//QRBlobを生成して、イベントオブジェクトに追加
e.qrcode = createQrBlob(e.receptionUrl);
//メールを送信
sendEmail(e);
//IDをフォームの回答 1 スプレッドシートに記録
// e.range.getCell(1, 1).offset(0, -1).setValue(e.id); //A列に記録
//NOTE: 範囲→単一セルを選択する方法 Range.getCell(相対行,相対列) 範囲外の場合はoffset()
//NOTE: 同じく Range.getNextDataCell(SpreadsheetApp.Direction.PREVIOUS) でも動く、けど、getCellのほうが多分動作が軽い(スプシ読む必要無いので)
//NOTE: https://caymezon.com/gas-cell-range/#toc2
//出席管理 スプレッドシートに記録 転記
const array = e.values;
array.push(e.id);
answerSHT.appendRow(array);
SpreadsheetApp.flush();
}
e.range.getCell(1, 1).offset(0, -1).setValue(e.id); は、生データとなる「フォームの回答 1」に書き込むので、コメントアウトしてあります。これをイキにする場合は、「フォームの回答 1」のA列にID書き込む列をいれてください。
array.push
このコードを書けたときは、これがpushかー!と腑に落ちたものです。見て!お母さん、あたし、pushできた!配列に追加できたよ!お母さん見て!見て!ねー、見てよー!そんな感じです。
idCreator
/**
* idCreator ファンクション:新規データのIDを作成する(e.range.rowStartでもいいけど、ちょっと遊びを)
* @param {object} e フォーム回答イベントのイベントオブジェクト
* @param {number} len IDの長さ 初期値3、1→上限26個、2→上限676個、3→上限17,576個、4→上限456,976個、5→上限11,881,376個
* @param {string} prefix IDの接頭文字を指定する
* @param {string} surfix IDの接尾文字を指定
* @return {string} id 完成したID
*/
function idCreator(e, len = 3, prefix = "", surfix = "") {//仮引数prefix は、「デフォルト値」。e.id = idCreator(e, 3, today)でtodayを渡されると prefixはtodayになります。
const count = e.range.rowStart - 1;
//ID上限チェック
if (count > 26 ** len) throw `count[${count}]は、IDの長さ[${len}]で作成可能な上限[${26 ** len}]を超えています。`;
let id = "";
for (let i = len; i > 0; i--) {
id += String.fromCharCode(Math.floor((count - 1) % (26 ** i) / 26 ** (i - 1)) + 65); //65=A Aから始まるように
}
id = prefix + id + surfix;
return id;
}
いや~すごいな~、ここはKさんの力量が発揮されていますね。アルファベット組み合わせを生成しています。自分ではソラで書けないな...。
関数の引数に=で直接指定できるってのも最近知りました。テストで動かしたい時にも使える。
デフォルト引数も使いこなしててすごい。
String.fromCharCode()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode
createQrBlob
/**
* createQrBlob イベントオブジェクトから、QRコードを生成してBLOBを返す
* @param {object} e イベントオブジェクト
* @return {blob} QRコードのBlob
*/
function createQrBlob(data) {
const qrCreateURL = `https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=${encodeURI(data)}`;
const option = {
method: 'get',
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(qrCreateURL, option);
return response.getBlob().setName('QRコード.png');
}
↑QRコード部分の元ネタ。
Blobもっとちゃんと理解したい。
createReceptionUrl
/**
* フォームの回答から、受付用URLを生成する
* @param {object} e イベントオブジェクト
* @return {string} url 受付用URL
*/
function createReceptionUrl(e) {
const baseUrl = PropertiesService.getScriptProperties().getProperty('RECEPTION_BASEURL');
return `${baseUrl}?id=${e.id}`;
}
後述する「5.デプロイしたURLをスクリプトに反映」でここを使うよ!
QR用のURLにIDをparameterとしてくっつけてる。
sendEmail
/**
* イベントオブジェクトからメールを送信する
* @param {object} e イベントオブジェクト
*/
function sendEmail(e) {
const refUrl = 'https://www......';
//HTMLメールが表示出来ない場合の代替本文を作成
const bodyTemplate = `
${e.namedValues['氏名']} 様
この度は 受付フォーム にご回答いただきありがとうございます。
あなたの受付番号は「${e.id}」です。
当日のイベントについては、こちらのURLをご覧ください:
${refUrl}
添付ファイルのQRコードを受付でご提示下さい。
会社名 xxxx
イベントurl xxxx
メールアドレス xxxx
`;
const recipient = e.namedValues['メールアドレス']
const subject = 'xxxに登録されました';
const inlineImageCode = "<strong>イベント当日は、このQRコードを受付に提示してください。<strong><br><img src='cid:inlineImg'>";
const htmlBody = bodyTemplate.replace(/[\r\n]/g, '<br>')
.replace('添付ファイルのQRコードを受付でご提示下さい。', inlineImageCode);//htmlに置き換え
const options = {
htmlBody: htmlBody,
inlineImages: {
inlineImg: e.qrcode
},
attachments: e.qrcode
};
// ランダムでスリープかける。同時申込でメール送信できない現象回避のため。
const max = 30;
const min = 0;
const staySecond = Math.floor(Math.random() * (max - min + 1) + min);
Utilities.sleep(staySecond * 1000);
//メール送信
GmailApp.sendEmail(recipient, subject, bodyTemplate, options)
}
メール送信の本体部分。htmlでない場合も想定して、画像添付でもQRを送ったりしていて、気が利いてる。
秒単位で同時申込の際の回避として、苦肉の策でsleep入れてるが、なんかいい方法あるのかなあ。
htmlメールはsendEmailのoptionsで指定するんですね~!
乱数
講座のこれ!これをまるっとコピペして活用しました。
メール文章は、constしてないで直接書いてもいいかもね。
const refUrl = 'https://www......'; //const しないで直接本文に書いてもいいかもね。
Test
function test() {
const e = {
namedValues:
{
'メールアドレス': ['XXXX@gmail.com'],
'テストオプション': ['選択肢 1'],
'タイムスタンプ': ['2021/11/28 20:59:05'],
'氏名': ['テスト太郎']
},
range: { columnEnd: 4, columnStart: 1, rowEnd: 3, rowStart: 3 },
source: {},
triggerUid: '9187333',
values:
['2021/11/28 20:59:05',
'XXXX@gmail.com',
'選択肢 1',
'テスト太郎']
}
// const name = e.namedValues['氏名'];
// console.log(name);
retrieveFormAnswer(e);
}
これは本番コードにはなくてもいいんですが、自分のメモとして。
毎回フォームに入力するのが面倒だから、テスト用の回答をconstしておくという。そんなことできるんですね~。
で、この元データとなるイベントオブジェクトをどっから確認するかというと。
retrieveFormAnswerの関数にこんなコードを入れていて、フォーム回答してみましょう。
doGetコード
さて、もう一つのスクリプトファイル、doGetです。
メイン処理とテキスト部分で関数を分けてます。
テキスト部分は、使いまわしていくと改変する可能性が高いので、関数分けておいたほうが可読性がよさそう、とのアドバイスありがとうございます!
/**
* 出欠状況(登録済かどうか)を画面に表示する。
* 出席管理シートには、QRを最初に読み込んだ時の日時を反映する。
* doGetシートには、毎回のQRを読み込んだ時の日時を反映する(一つのQRコードを何度も読み込んだ場合、同一IDのログが残る)
* @param {object} e イベントオブジェクト
* @return {string}出欠状況(登録済がどうか)を画面に表示する
*/
function doGet(e) {
console.log(e)
const ss = SpreadsheetApp.getActiveSpreadsheet();
const id = e.parameter.id;
const time = Utilities.formatDate(new Date(), 'JST', 'yyyy/MM/dd HH:mm:ss aaa');
const recordSHT = ss.getSheetByName('doGet');
const recordVAL = [id, time];
recordSHT.appendRow(recordVAL);
const answerSHT = ss.getSheetByName('出席管理');
const asnwerRNG = answerSHT.getDataRange();
const answerVALS = asnwerRNG.getValues();
const idCOL = answerVALS[0].indexOf('ID');
const attendCOL = answerVALS[0].indexOf('出席');
const nameCOL = answerVALS[0].indexOf('氏名');
let textOutput = msgTextGet();//msgTextGet関数からテキスト本文を呼び出し
for (const answer of answerVALS) {
//IDがヒットした時だけ処理する
if (answer[idCOL] === id) {
textOutput = textOutput
.replace(/{{id}}/, answer[idCOL])
.replace(/{{氏名}}/, answer[nameCOL]);
//まだ出席登録されていない
if (!answer[attendCOL]) {
answer[attendCOL] = time;
textOutput = textOutput
.replace(/{{本文}}/, '出欠登録を受け付けました。')
.replace(/{{時刻}}/, time);
//すでに出席登録されている
} else {
textOutput = textOutput
.replace(/{{本文}}/, '出欠登録は受付済みです')
.replace(/{{時刻}}/, `(初回受付)${answer[attendCOL]}`);
}
break;
}
}
asnwerRNG.setValues(answerVALS);
return ContentService.createTextOutput(textOutput);
}
/**
* 画面に表示するテキスト本文を生成する。
* @return {string}テキスト
*/
function msgTextGet() {
const textOutput = `
ご来場ありがとうございます!
{{本文}}
ID:{{id}}
氏名:{{氏名}}
時刻:{{時刻}}
`;
return textOutput;
}
このへんは好みがいろいろあるかと思いますが、24時間表記にしてさらにAM/PMつけておいた。
const time = Utilities.formatDate(new Date(),'JST','yyyy/MM/dd hh:mm:ss');
表示例 2021/12/01 03:44:27
const time = Utilities.formatDate(new Date(),'JST','yyyy/MM/dd HH:mm:ss aaa');
表示例 2021/12/01 15:44:27 PM
https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
setNote
好みで、更に編集時の履歴を残すスクリプトを入れてもいいかもしんない。
これ、まさにGAS中級講座でやったやつ…!
コードについて
eに加えてくのは、大規模になると危険。
e.id?あれ、リファレンスにねーぞ?
みたいになる。らしい。
このぐらいの小規模コードなら、まあ、辿れるからいいかな…?
Lock service
Lockサービスの手も検討に上がったが、今回は見送り。このへんまだ理解できてないな~。
Lockサービスについては、ノンプロ研GAS中級講座
200228中級講座GASコース-04
200313中級講座GASコース-05 あたりのスライドにもあるぞい!
ノンプロ研メンバーはwiki(のーしょん)から確認できますよ。
4.doGet デプロイ
doGetのスクリプトをウェブアプリとしてデプロイします。アクセスできるユーザを自分のみにすることで、QRを読み込んだときの受付は自分だけが行える、という形。
doGetのスクリプトを書き換えたときは、めんどうですが、デプロイしなおして、URL取得して、後述するスクリプトに反映するものを書き直します。
doGet なんだそれは状態だったんですが、
doGetについても「いつも隣にITのお仕事(通称 隣IT)」のブログを参考にしました。
5.デプロイしたURLをスクリプトに反映
もうちょっとですよ!
デプロイしたURLをスクリプトに反映しましょう。
さきほどのURLをRECEPTION_BASEURLとしてプロパティストアに格納します。
直書きでもいいんですけどね。
メインのスクリプトのcreateReceptionUrl関数のところです。
function setScriptProperties() {
const properties = PropertiesService.getScriptProperties();
properties.setProperty('RECEPTION_BASEURL','https://*****');
}
const baseUrl = PropertiesService.getScriptProperties().getProperty('RECEPTION_BASEURL');
const baseUrl = 'ここにURLいれる'; //直接URL書く場合
プロパティストアの概要とスクリプトプロパティの入力方法
スクリプトプロパティを操作してそのデータを取り出す方法
doGetのスクリプト書き直したら、再度デプロイして、また別URLになるのでそれをメインコードに反映することをお忘れなく~。
6.トリガーの設定
メインの関数が、フォーム送信時に動くよう、トリガーを追加します。
7.できた
フォームから実際に申し込んでみて、メールが送られて、まずここで嬉しい!そんで、QR読み込んで受付できた、ここでまた嬉しい!
やった~~~できた~~~!
シートにすぐに受付番号が反映されないときもあるので、そんな時は数秒待ってください。
実際、使ったときの画面については次の「使い方、受付方法の概要」で画像で示す。
使い方、受付方法の概要
デプロイした人のアカウントでQRを読み込めば受け付けできます。
スマホとかで読み込めばおけ。
運用上の諸注意
受付でQRコード出せない人の対応
どのメールかわからんとか、スマホの充電が切れたとか、印刷したけどその紙を持ってくるの忘れたとか、とにかくQRを読み込むことができない人が一定数発生するので、そういう人はシートから名前で探して、直接シートに書き込むなどして受け付ける必要があると思われる。
出欠のログについて
上記の図のように、
出席管理のシート では、最初の出席日時が反映される。
doGetのシートには、重複含めてすべての受付日時が反映される。
QRを読み込んだ時に、既に一度受付済ならその旨が画面には表示される。
一つのQRを何度も読み込むことはあまり無いかもしれないが、その点、ご注意ください。
メール
メール届かない問題がある。
ひとつは、迷惑メールに振り分けられているケース。
もうひとつは、秒単位で同時多数の申込みがあったとき、
迷惑メールについては、チェックしてくださいね~という案内である程度なんとかなる。
申込フォーム同時多数の受付について
秒単位同時申込については、なかなか検証が難しい。ランダム秒でsleepを噛ませて、メール送信スクリプトの起動を多少ずらすことで何とか無理やり対応している。これが秒単位に数十件、数百件となったときにどうなるかはわからない。このあたりのGASの割り当て、使用制限については後述する。
どちらにしても、メールが届かない場合はお問い合わせください、という予防線を張っておくとよさそう。
送信したメールはデプロイしたユーザの送信トレイにはあるし、送り直してあげるとか、リスト見て受け付けてあげる、といった対応が必要かと思う。
GASの割り当て・使用制限
Usage limitsのページを見ると、
1.
Per user rate limit
250 quota units per user per second, moving average (allows short bursts).
ユーザーあたりのレート制限
ユーザーあたり1秒あたり250クォータユニット、移動平均(短いバーストを許可)。
2.
messages.sendメソッドの使用で100のQuota Unitsを消費するらしい
なので、1と2を勘案すると、1秒あたり250MAXのquotaに対して、100quota必要なmessages.sendは2回の処理が限界ということかなあ。だとすると、下記のサイトとも合致する。
乱数発生でスリープかけて、なんとかある程度は対処できているかなとは思うが、堅牢性が高いとは言えないので、そのへんはご了承ください。
Usage limits
https://developers.google.com/gmail/api/reference/quota
Quotas for Google Services
https://developers.google.com/apps-script/guides/services/quotas
Google Workspace における Gmail の送信制限https://support.google.com/a/answer/166852?hl=ja
謝辞
ノンプロ研の皆様に感謝申し上げます。
連日ペアプロやモブプロにお付き合い頂き、新たな視点を得ることができました。
slackやTwitterでの交流にも大変助けられました。
ひとりではここまで来れませんでした。
マリアナ海溝よりも深く、深〜〜〜く御礼を申し上げます。
ありがとうございます!!!!!
自分でもいけてるコードをもっと書きたい~~~。
卒業LTとBT頑張ります。
あ、あと宿題…出してない…ひーー
SpecialThanks
メンション忘れがあったらすみません…教えてください…!
@etau0422
https://note.com/etau/
@kanimiso_gs
@GasNao703
https://note.com/gasnao703/
@black777cat
@ID_HelpDesk
https://note.com/id_helpdesk/
@InvestorVet_
@NAOP4P4
名前を挙げきれませんが、ノンプロ研のみなさま、本当にありがとうございます!!!
参考URL
スミマセン、覚えてないものや、リストアップ忘れがあります……。
Google Apps ScriptでQRコードを生成してみるhttps://note.com/himajin_no_asobi/n/n51de21bf73e5?fbclid=IwAR0r0CSmn5x1a0wwBIprGCJdv-OZXaduNpxsvDY2mmlwi4gBhLzOq3bgCEg
GASでHTMLメールを送る方法とインライン画像を埋め込む(画像挿入)方法
https://auto-worker.com/blog/?p=2827
GAS:シート内の改行を削除する
https://uske-s.hatenablog.com/entry/2018/04/02/165444
GASでGetパラメータを受け取ってスプレッドシートに書き込む方法https://qiita.com/hirohiro77/items/a947416f803f45777338?fbclid=IwAR0TEWPs9Gh291IG7ZGgWaWFKrvKNgqaOrFUYw2XYur3mWUAmQOUrr-dUs4
GAS, Spreadsheet, QR-Codeで受付システムを作ってみたhttps://qiita.com/HYuta999/items/8858c062d3a2aaadea8c?fbclid=IwAR2_d5_vUXyKnIhMH92dGANo60dyCHO32sUyrXslODxC5Arox-I5CS4ztIQ
GASでQRコードを使った同人頒布会向け予約システムを作った話https://qiita.com/blachocolat/items/47d324e91339c60e5397?fbclid=IwAR02dGfGJPlf5tIUPzTjPsoaGIS1DNZ_8HgnRfPViS17pr1HUgxZ8hMP_hI
Google Apps Scriptコーディングガイドライン【随時更新】(ドキュメンテーションコメントの書き方)https://tonari-it.com/gas-coding-guide-line/
【初心者向けGAS】プロパティストアの概要とスクリプトプロパティの入力方法https://tonari-it.com/gas-property-store/#toc6
【初心者向けGAS】スクリプトプロパティを操作してそのデータを取り出す方法https://tonari-it.com/gas-properties-script-property/
GASでスクリプトの処理を一時的に遅延させるsleep
https://blog.8basetech.com/google-apps-script/gas-sleep/
Quotas for Google Services
https://developers.google.com/apps-script/guides/services/quotas
Usage limits
https://developers.google.com/gmail/api/reference/quota
Google Workspace における Gmail の送信制限https://support.google.com/a/answer/166852?hl=ja
Gmail APIとPythonを使ってメール送信を自動化する方法(Gmail APIの利用制限)
https://valmore.work/automate-gmail-sending/
GASの排他制御(ロック)の利用方法を調べたhttps://qiita.com/kyamadahoge/items/f5d3fafb2eea97af42fe
とゆーわけで、次のアドベントカレンダー かにみそ先生にお渡しします!