2026.10.04

GASでMeetのURLを発行する方法|乱数で作った偽URLの失敗

GASGoogle Meet業務自動化

GASでMeetのURLを発行する方法|乱数で作った偽URLの失敗

「予定を登録し、Meet付きの案内文をクリップボードにコピーしました!」

自社で使っているデスクトップの常駐アシスタントが、こう表示していました。件名と日時を入れてボタンを押すと、Googleカレンダーに予定が入り、取引先に送る案内文まで出来上がる。そういう機能です。

ところが、その案内文に入っていたGoogle MeetのURLは、どこにも存在しない会議室のURLでした。プログラムが乱数(Math.random())でそれらしい文字を並べ、Meetのリンクと同じ形に整えていただけだったのです。

私(UMEO CREATE・伊藤)の環境で、2026年8月に実際にあった話です。この記事では、何が起きていたか、Google Apps Script(GAS。Googleのサービスを動かすための簡易プログラム環境)で本物のMeetのURLを発行する正しい書き方、そして「発行できなかったとき」にどう振る舞わせるかを、いま動いているコードつきでまとめます。GASで予定登録や案内メールを自動化している方、これから作る方に向けた記事です。

夕方の事務所で、スマートフォンを片手に持ったまま会議の開始を待って腕時計を見る女性
夕方の事務所で、スマートフォンを片手に持ったまま会議の開始を待って腕時計を見る女性
この記事の目次(6項目)

何が起きていたか:形だけ正しいMeetのURL

Google Meetの参加URLは、meet.google.com/ のあとに、英小文字が3文字・4文字・3文字とハイフン区切りで並びます。見慣れた形です。

問題の機能は、この形だけを自分で作っていました。考え方としては次のようなものです(当時のコードそのものは残っていないので、仕組みを示すための再現イメージです)。

// ★やってはいけない例(再現イメージ)
const pick = n => Array.from({ length: n },
  () => 'abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)]).join('');
const meetUrl = `https://meet.google.com/${pick(3)}-${pick(4)}-${pick(3)}`;

これで出来る文字列は、見た目は完全にMeetのURLです。クリックすればGoogleのページにも飛びます。ただし、その会議室はGoogle側に作られていません。受け取った相手は入室できません。

時系列を整理します。

  • 2026年8月18日:アシスタントを作り直した回で、「予定とMeetをまとめて登録する」ボタンが入る
  • 2026年8月21日:URLを自前で組み立てていることに気づき、修正
  • 2026年8月22日:テスト用の予定を実際に作り、発行されたURLがカレンダー側の会議URLと一致することを確認して、予定を削除

怖いのは、この不具合が3つ重なっていたことです。

1. 実在しないURLを作っていた
2. それを「お世話になっております。UMEO CREATE 伊藤です」で始まる取引先向けの案内文に組み込み、クリップボードへコピーしていた
3. そのうえで成功のメッセージを出していた

エラーが出れば気づけます。画面が固まれば直します。しかしこの機能は、何ひとつ失敗の合図を出さず、「出来ました」と言っていました。

正直に書いておくと、この案内文が実際に取引先へ送られたかどうかは、断定できません。この記事を書くにあたって、8月18日から23日の送信済みメールを案内文の見出しで検索しましたが、該当するメールは見つかりませんでした。ただ、クリップボードにコピーされた文面をメール以外で貼ったかどうかまでは、記録が残っていません。「送った形跡は見つからなかった。けれど、8月18日から21日までの足かけ4日間、送れる状態で置かれていた」。分かっているのはここまでです。

なぜ「それらしい値」が紛れ込むのか

これはMeetに限った話ではありません。

プログラムを作る途中では、まだつながっていない部分に仮の値を置くことがよくあります。画面の動きを先に確かめたいので、本物の代わりにそれらしい値を入れておく。あとで本物に差し替えるつもりで。

仮の値が「テスト用URL」や「ここにURLが入ります」のように、ひと目で仮と分かる形なら害はありません。危ないのは、本物と見分けがつかない形をしているときです。差し替えを忘れても、誰も気づけません。

たとえるなら、まだ電話を引いていない新しい事務所の名刺に、とりあえず「それらしい桁数の番号」を刷ってしまうようなものです。名刺としては完成して見えます。でも、かけてもつながりません。しかも、つながらないことを知るのは、名刺を渡した自分ではなく、受け取った相手です。

会議のURL、注文番号、追跡番号、予約番号、請求書番号。これらに共通するのは、発行する権限を持つ相手が決まっていることです。MeetのURLを発行できるのはGoogleだけです。形を知っているからといって、自分で作ってよいものではありません。

新しい事務所の机で、刷り上がったばかりの名刺の束を手に取り、1枚を見つめる男性
新しい事務所の机で、刷り上がったばかりの名刺の束を手に取り、1枚を見つめる男性

GASでMeetのURLを正しく発行する方法

ここからが本題です。GASでカレンダーに予定を入れ、本物のMeetのURLを受け取る書き方です。

CalendarAppではMeetを作れない

GASでカレンダーを扱うとき、まず使うのは CalendarApp です。予定の作成も取得もこれで出来ます。ところが、CalendarApp にはMeetの会議を作る機能がありません。createEvent で予定は入りますが、会議リンクは付きません。

ここで手が止まると、「では形だけ自分で作ってしまおう」という誘惑が生まれます。今回の不具合の入口は、おそらくここでした。

Advanced Calendar Serviceを有効にする

Meetを発行できるのは、Advanced Calendar Service(Calendar API v3をGASから呼ぶための拡張機能)です。GASのエディタ左側の「サービス」から「Google Calendar API」を追加します。設定ファイル appsscript.json には、次の記述が入ります。

"dependencies": {
  "enabledAdvancedServices": [
    { "userSymbol": "Calendar", "serviceId": "calendar", "version": "v3" }
  ]
}

私の環境では、もともとカレンダーを読み書きする権限で動いていたので、この追加で権限の再承認を求められることはありませんでした(権限の構成によっては再承認が出ます)。

conferenceData.createRequest と conferenceDataVersion:1

予定を作るときに、「会議も作ってください」という依頼(conferenceData.createRequest)を添えます。そして、呼び出しのオプションに conferenceDataVersion: 1 を付けます。この2つがそろって初めてMeetが発行されます。 オプションを付け忘れると、依頼は黙って無視され、会議なしの予定だけが出来ます。

いま動いているコードの要点です。

function createEventWithMeet(body) {
  // 日時は必ず +09:00 を明示する(素の new Date('2026-10-04') はUTC扱いになる)
  var time = String(body.time);
  var hhmm = time.length === 4 ? '0' + time : time;   // 9:00 を 09:00 にそろえる
  var startIso = body.date + 'T' + hhmm + ':00+09:00';
  var startMs = new Date(startIso).getTime();
  var dur = Number(body.durationMinutes) || 60;
  var endIso = Utilities.formatDate(
    new Date(startMs + dur * 60 * 1000), 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ss") + '+09:00';

  var resource = {
    summary: body.title,
    start: { dateTime: startIso, timeZone: 'Asia/Tokyo' },
    end:   { dateTime: endIso,   timeZone: 'Asia/Tokyo' },
    conferenceData: {
      createRequest: {
        requestId: Utilities.getUuid(),              // 依頼ごとに一意の値
        conferenceSolutionKey: { type: 'hangoutsMeet' }
      }
    }
  };

  var ev = Calendar.Events.insert(resource, 'primary', { conferenceDataVersion: 1 });

  var meetUrl = ev.hangoutLink || meetUrlOf_(ev);
  // 割り当てが登録直後に間に合わないことがあるので、一度だけ取り直す
  if (!meetUrl) {
    Utilities.sleep(1500);
    var again = Calendar.Events.get('primary', ev.id);
    meetUrl = again.hangoutLink || meetUrlOf_(again);
  }
  // 発行できなかったときは必ず空で返す
  return { ok: true, eventId: ev.id, htmlLink: ev.htmlLink || '', meetUrl: meetUrl || '' };
}

function meetUrlOf_(ev) {
  var eps = (ev && ev.conferenceData && ev.conferenceData.entryPoints) || [];
  for (var i = 0; i < eps.length; i++) {
    if (eps[i].entryPointType === 'video' && eps[i].uri) return eps[i].uri;
  }
  return '';
}

押さえる点は4つです。

  • requestId は依頼ごとに一意の値にする。 ここで使っている Utilities.getUuid() は乱数由来の値ですが、役割が違います。これは「この依頼を区別するための番号」をこちらが付けているだけで、URLそのものを作っているわけではありません。URLを決めるのはGoogleです
  • URLは hangoutLink から受け取る。 入っていないときは conferenceData.entryPoints の中から、種類が video のものを探します
  • 登録直後は会議室の割り当てが間に合わないことがある。 空だったら1.5秒待って、予定を一度だけ取り直します
  • それでも取れなければ、空のまま返す。 ここで「それらしい値」を埋めないことが、この記事でいちばん大事な一行です
町工場の事務スペースで、窓口の係から番号札を受け取る作業着姿の男性の手元
町工場の事務スペースで、窓口の係から番号札を受け取る作業着姿の男性の手元

発行できなかったとき、どう振る舞わせるか

本物を発行する書き方に直しても、発行に失敗する場面は残ります。通信が切れる、権限が足りない、割り当てが間に合わない。そのときプログラムにどう振る舞わせるかで、同じ事故がもう一度起きるかどうかが決まります。

呼び出す側(デスクトップのアシスタント)は、次の3つに分けました。

① カレンダーへの登録そのものに失敗したとき
案内文を作りません。「カレンダーに登録できませんでした」と理由つきで表示して終わります。登録できていない予定の案内文を作ると、存在しない打ち合わせを取引先に知らせることになるからです。

② 予定は入ったが、MeetのURLが取れなかったとき
案内文は作りますが、URLの行を入れません。表示はこうです。

予定は登録しましたが、Meetリンクを発行できませんでした。案内文にURLは入れていません(カレンダーから発行してください)。

③ どちらも成功したとき
ここで初めて「Meet付きの案内文をコピーしました」と表示します。

案内文を組み立てる部分は、URLがあるときだけ行を足す形にしています。

const meetUrl = (r && r.meetUrl) || '';
const inviteText =
  `【件名】${title}\n` +
  `【日時】${date} ${time}から(${dur}分間)\n` +
  (meetUrl ? `【Google Meet参加URL】\n${meetUrl}\n` : '');

「取れなかったら空」「空なら載せない」「載せなかったことを人に伝える」。この3つがそろって、ようやく安全になります。どれか1つでも欠けると、空欄のまま送る、古い値が残る、といった別の事故に形を変えるだけです。

自分のコードに同じ穴がないか確かめる5つ

直したあと、ほかの機能にも同じ種類の穴がないかを見直しました。そのとき使った観点です。

  • 外部のサービスが発行するはずの値を、自分のプログラムで作っていないか。 コードの中で Math.random や、日付から番号を作っている箇所を検索し、その値がどこへ出ていくかを追います。まばたきの間隔のような「見た目の演出」に使う乱数は問題ありません。相手に渡る値に使っていないかを見ます
  • 取引先に出る文面に入る値は、すべて出どころを言えるか。 URL、日時、金額、番号。1つずつ「これはどこから来た値か」を答えられるようにします
  • 失敗したときに、成功と同じメッセージが出ていないか。 わざと通信を切る、権限を外すなどして、失敗させてみます
  • 「取れなかったとき」の値が空になっているか。 仮の値や前回の値が残らないかを確かめます
  • 出てきた値を、発行元で突き合わせたか。 今回は、テストの予定を作り、返ってきたURLとGoogleカレンダーに表示される会議URLが一致することを見てから、予定を消しました

最後の1つは、手間に見えて実は数分です。「動いた」ではなく「本物だった」を確かめる。 画面に成功と出ただけでは、今回の不具合は見抜けませんでした。

なお、同じアシスタントのGAS側では、もうひとつ別の落とし穴も踏んでいます。GASの doPost はリクエストのヘッダーを読めないため、LINEの署名検証が実装できない、という話です。こちらはGASのdoPostはヘッダーを読めない。LINEの署名検証の代わりに何で守ったかに書きました。

倉庫の棚の前で、手元の伝票と箱の札を指さしで照らし合わせる女性
倉庫の棚の前で、手元の伝票と箱の札を指さしで照らし合わせる女性

まとめ:「それらしい値」は、エラーより怖い

エラーは親切です。止まってくれるので、直すきっかけになります。怖いのは、止まらずに、正しそうな顔で通ってしまう値のほうでした。

  • MeetのURLは、形を知っていても自分で作らない。発行できるのはGoogleだけ
  • GASでは CalendarApp ではなく、Advanced Calendar Serviceの conferenceData.createRequest と conferenceDataVersion: 1 で発行する
  • 取れなかったら空で返し、案内文に載せず、載せなかったことを表示する
  • 直したら、発行元の画面と突き合わせて「本物だった」を確かめる

事務の自動化は、うまく動いているように見える期間がいちばん長く続きます。だからこそ、取引先に出ていく値だけは、出どころを1つずつ確かめる。私はいま、案内文やメールは下書きまでを機械に作らせ、送信は必ず自分の手で行っています。今回の件のあと、この一線の意味をあらためて感じました。

UMEO CREATEでは、御社に伺い、御社のパソコンにAIが働く環境を半日でつくる「AIエージェント導入支援」を行っています。お渡しするのは、今回のような失敗と手直しを経て、私が自社で毎日使っている仕組みが元になったものです。

▼ AIエージェント導入支援について
https://www.umeocreate.com/agent.html


【群馬・北関東の中小企業・ひとり社長のAI導入をサポート】

UMEO CREATE(ウメオクリエイト)では、群馬県(高崎・前橋・太田・伊勢崎・吉岡町など)を中心に、中小企業や個人事業主・ひとり社長様向けの「AI導入・業務自動化・DX伴走コンサルティング」を行っています。

「自社の業務でもAIを使える?」「何から手をつければいいかわからない」「面倒な事務作業を自動化したい」といったご相談は、お気軽に公式LINEまたはWebサイトのお問い合わせフォームよりご連絡ください。

「うちの何にAIを使えるのか」から決めたい方へ

17問の選択式に答えるだけで、どの業務から手をつけるかがその場で分かる 「3分でわかる 業務のムダ診断」を無料で公開しています。連絡先の入力はありません。

3分の診断をやってみる(無料・連絡先なし) 紙のチェックリスト(PDF)がよい方はこちら

群馬県内でのAI導入・業務システム開発・AI研修のご相談を承っています。「何から始めればいいか分からない」段階からで構いません。下のフォームからお気軽にどうぞ。

← コラム一覧へ戻る

「AIで何ができるか分からない」
その段階のご相談こそ、歓迎です。

現状の業務の困りごとを伺うところから始めます。売り込みはしません。お気軽にどうぞ。

1営業日以内にご返信いたします。

フォームのほか、LINEでもご相談を承っています
LINEで相談する
友だち追加後、そのままトークでご質問いただけます

営業・ご提案のご連絡も、こちらのフォームからお願いいたします。
お電話での営業はお受けしておりません。