Amazonで商品を見るセール会場へ

GoogleカレンダーをLINEへ通知するには|Notify終了後の選択肢と練習コード

18445_google-calendar-line-notify

LINE Notifyは2025年3月31日に終了し、トークンの新規発行と旧APIの通知は利用できません。

旧LINE Notifyの設定をやり直しても復旧しないため、現在の受け取り方を選び直します。

終了日はLINEの公式終了案内を2026年10月2日に確認しています。

目次

自分の予定通知とLINEへの送信を選び分ける

目的現在の方法
予定の15分前に自分が気づきたいGoogleカレンダー標準の通知を設定する
既存のLINE公式アカウントから自分へ送るMessaging APIの送信条件を確認して連携する
旧LINE Notifyのトークンを再発行したい提供終了のため再発行・旧API利用はできない

自分の予定を見落とさないことが目的なら、Googleカレンダーで予定を開き、編集画面の通知へ15分前を設定して保存します。

パソコンでは通知方法とブラウザーの通知許可を確認します。デスクトップ通知を受け取る間は、ブラウザーでGoogleカレンダーを開いたままにしてください。

操作の説明はGoogleの通知設定ヘルプに基づきます。

予定を数分先に作って受信を確かめ、届かない場合は端末・ブラウザーの通知許可と対象カレンダーの設定を確認します。

スポンサーリンク

Messaging APIへ移す前の練習

LINEに送る場合は、Messaging APIを有効にした既存の公式アカウント、チャネルアクセストークン、送信条件を満たす個人ユーザーIDが必要です。

旧LINE Notifyのトークン、LINEの表示名、Googleのメールアドレスで代用できません。

送信条件とプッシュ通知はLINEの送信ガイドで確認します。

この例は既存チャネルを使う個人宛ての練習で、グループ、アカウント作成、契約変更は扱いません。

通知連携の練習コードとローカルテストをダウンロード(ZIP)。

各article-番号.jsを別々のApps Scriptプロジェクトへ保存します。

まずarticle-18445.jsを保存し、demoLinePayloadを実行します。

架空の予定だけから送信用JSONを作り、実際のカレンダーやLINEへアクセスしません。

// 各記事の例は別々のApps Scriptプロジェクトへ保存します。
function props_() { return PropertiesService.getScriptProperties(); }
function required_(key) {
  const value = props_().getProperty(key);
  if (!value) throw new Error(key + 'をスクリプトプロパティへ設定してください');
  return value;
}
function plain_(text) {
  return String(text).replace(/\[/g, '[').replace(/\]/g, ']');
}
function stateKey_(kind, destination, id) {
  const digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,
    kind + ':' + destination + ':' + id, Utilities.Charset.UTF_8);
  return 'CGM_JOB_' + Utilities.base64EncodeWebSafe(digest);
}
function preview_(plans) {
  // 実メールや予定の本文はログへ出さない。demo関数で架空の本文を確認。
  Logger.log(JSON.stringify(plans.map(p => ({id: p.id, characters: p.text.length}))));
  return plans;
}
function sendControlled_(kind, destination, buildPlans, transport) {
  if (props_().getProperty('SEND_ENABLED') !== 'true') throw new Error('送信は無効です。まずプレビューしてください');
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(1000)) throw new Error('別の実行が処理中です');
  try {
    const plans = buildPlans();
    for (const plan of plans) {
      const key = stateKey_(kind, destination, plan.id);
      const state = props_().getProperty(key);
      if (state === 'done') continue;
      if (state) throw new Error('結果不明の処理があります。投稿先と記録を照合してください');
      // 通信前に記録。通信成功後の記録失敗でも自動で再送しない。
      props_().setProperty(key, 'sending');
      try {
        transport(plan.text);
        props_().setProperty(key, 'done');
      } catch (error) {
        props_().setProperty(key, 'unknown');
        throw new Error('送信結果を確定できません。自動再送せず投稿先を確認してください');
      }
    }
  } finally { lock.releaseLock(); }
}
function calendarPlans_(events, now, minutes) {
  const limit = now.getTime() + minutes * 60000;
  return events.filter(e => !e.allDay && e.start > now.getTime() && e.start <= limit)
    .sort((a, b) => a.start - b.start)
    .map(e => ({id: e.id + ':' + e.start,
      text: '【予定の通知】' + Utilities.formatDate(new Date(e.start), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm')
        + ' ' + plain_(e.title) + (e.location ? '\n場所: ' + plain_(e.location) : '')}));
}
function readCalendarPlans_() {
  const now = new Date();
  const minutes = 15;
  const calendar = CalendarApp.getCalendarById(required_('CALENDAR_ID'));
  if (!calendar) throw new Error('カレンダーを参照できません');
  const events = calendar.getEvents(now, new Date(now.getTime() + minutes * 60000)).map(e => ({
    id: e.getId(), start: e.getStartTime().getTime(), title: e.getTitle(),
    location: e.getLocation(), allDay: e.isAllDayEvent()
  }));
  return calendarPlans_(events, now, minutes);
}
function demoCalendar() {
  const now = new Date('2026-10-02T23:50:00+09:00');
  const events = [{id:'demo-A',start:new Date('2026-10-03T00:00:00+09:00').getTime(),title:'練習会',location:'練習室',allDay:false},
    {id:'demo-B',start:now.getTime(),title:'開始済み',location:'',allDay:false}];
  const plans = calendarPlans_(events, now, 15);
  Logger.log(JSON.stringify(plans));
  return plans;
}
function previewCalendar() { return preview_(readCalendarPlans_()); }
function linePayload_(plans, userId) {
  if (!/^U[0-9a-f]{32}$/i.test(userId)) throw new Error('対象は既存チャネルの個人ユーザーIDです');
  return plans.map(p => ({id: p.id, payload: {to: userId, messages: [{type:'text',text:p.text}]}}));
}
function demoLinePayload() {
  const payloads = linePayload_(demoCalendar(), 'U00000000000000000000000000000000');
  Logger.log(JSON.stringify(payloads));
  return payloads;
}
function sendCalendarToLine() {
  const userId = required_('LINE_USER_ID');
  linePayload_([], userId);
  const token = required_('LINE_CHANNEL_ACCESS_TOKEN');
  sendControlled_('line-calendar', userId, readCalendarPlans_, text => {
    const response = UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', {
      method:'post',contentType:'application/json',headers:{Authorization:'Bearer ' + token},
      payload:JSON.stringify({to:userId,messages:[{type:'text',text:text}]}),muteHttpExceptions:true
    });
    if (response.getResponseCode() !== 200) throw new Error('LINE HTTP ' + response.getResponseCode());
  });
}

日付をまたぐ予定の確認結果

練習条件ローカル模擬テストの結果
10月2日23:50から15分以内の10月3日0:00の予定1件を送信候補にする
開始済み・終日・15分より先の予定候補に含めない
同じ繰り返し予定の別の開始時刻予定IDと開始時刻の組で区別する
旧NotifyトークンをユーザーIDへ指定形式チェックで拒否する

実カレンダーを確認するときはCALENDAR_IDだけを設定してpreviewCalendarを実行します。

LINEへ実送信する前にLINE_USER_IDとLINE_CHANNEL_ACCESS_TOKENを設定し、送信先を確認します。

送信関数はsendCalendarToLineです。

時刻はDateの絶対時刻で比較し、表示をAsia/Tokyoにしています。

海外向けに表示する場合はUtilities.formatDateのタイムゾーンを目的の地域へ変え、日付をまたぐ予定で再確認します。

海外の表示変更は今回未検証です。

15分以内に始まる未通知の予定を処理するため、厳密に15分前の配信を保証するものではありません。

実行が止まっている間に開始した予定は対象外です。同じ予定ID・開始時刻で一度送信した予定は、件名や場所だけ変更しても再通知しません。長い件名や場所でAPIの文字数上限を超える場合や、月間の送信枠を超える場合にも対応が必要です。

通知方法を選ぶチェックリスト

予定を受け取りたい目的から順に当てはめると、使う方法が決まります。LINE公式の終了案内(2026年10月2日確認)とGoogleの通知設定ヘルプにもとづく目安です。

  1. まず、LINE Notifyが2025年3月31日に終了したことを確認する。トークンの新規発行と旧APIの通知は利用できず、旧LINE Notifyの設定をやり直しても復旧しない。
  2. 自分の予定を見落とさないことが目的なら、Googleカレンダーの標準の通知を使う。予定を開き、編集画面の通知に15分前などを設定して保存する。
  3. パソコンでは、カレンダーの設定で通知方法を確認し、ブラウザーの通知許可も確認する。
  4. LINEにも送りたいなら、Messaging APIを有効にした既存のLINE公式アカウント、チャネルアクセストークン、送信条件を満たす個人ユーザーIDを用意する。旧LINE Notifyのトークンでは代用できない。
  5. 設定したら、予定を数分先に作って受信を確かめる。届かないときは、端末・ブラウザーの通知許可と、対象カレンダーの設定を確認する。
  6. 送信を有効にする前に、送信条件とプッシュ通知の扱いを、LINEの送信ガイドで確認する。

この記事のLINEへの送信の練習は、既存のチャネルを使った個人宛てのものです。グループへの送信、アカウント作成、契約変更は扱いません。

送信を有効にする前の確認と止め方

練習先の自分が公式アカウントを友だち追加していることと、現在の送信条件・月間送信枠を確認します。送る内容と送信先を確認してから、スクリプトプロパティのSEND_ENABLEDをtrueにします。

デモやプレビューの関数に送信トリガーを付けません。

送信する関数を一度手動で試し、投稿先・内容・保存状態を照合してから、必要な時間主導トリガーを1つだけ設定します。

旧プロジェクトや別のトリガーが同時に送ると、このプロジェクトのロックだけでは二重投稿を防げません。

通信前にsending、HTTP 200応答後にdoneを保存します。doneはAPIの成功応答を記録した状態で、相手への到達や既読の確認ではありません。ブロック等でメッセージが届かなくても200が返る場合があります。

通信や記録に失敗した場合はunknownなどの状態を残して止まり、自動で再送しません。

結果が不明な場合はSEND_ENABLEDをfalseにし、トリガーも停止して、投稿先に同じ内容が届いていないか確認します。

送信が確認できた記録はdoneへ整え、未送信を確認できた場合だけ再実行の準備をします。

送信済みの記録を消すと同じ対象を再送する可能性があります。

長期運用では記録容量と整理方法を決め、旧コード・設定を控えてから変更します。

戻す際は新しいトリガーを止め、保存したコードと設定へ戻します。

練習コードの検証範囲

2026年10月2日に、Node.jsでGoogle Apps Script・HTTP応答・シート・ロック・保存状態を模した55ケースを確認しました。

ZIPを新しいフォルダーへ展開し、node test_integration.cjsでも同じ結果を確認できます。

実際のGoogle認可、APIへの投稿、シート書き込み、時間主導トリガー、受信側の通知音は未検証です。

ローカルでの成功を、実サービスでの配信成功と扱わないでください。

関連記事

スポンサーリンク

よくある質問:GoogleカレンダーとLINEの通知で迷ったときのチェックリスト

LINE Notifyは今も使えますか?

使えません。LINE Notifyは2025年3月31日に終了し、トークンの新規発行と旧APIの通知は利用できません。旧LINE Notifyの設定をやり直しても復旧しないため、現在の受け取り方を選び直します。

予定を見落とさないためだけなら、何を使えばよいですか?

Googleカレンダー標準の通知です。予定を開いて、編集画面の通知に15分前を設定して保存します。操作は、Googleの通知設定ヘルプにもとづきます。

LINEに通知を送るには何が必要ですか?

Messaging APIを有効にした既存のLINE公式アカウント、チャネルアクセストークン、送信条件を満たす個人ユーザーIDが必要です。

旧LINE Notifyのトークンを使い回せますか?

使い回せません。旧LINE Notifyのトークン、LINEの表示名、Googleのメールアドレスでは、Messaging APIの送信先を代用できません。

通知が届かないときは?

端末・ブラウザーの通知許可と、対象カレンダーの設定を確認します。予定を数分先に作って、受信を確かめてください。

記事のコードは実際にLINEへ送りますか?

記事の練習コードの最初の実行(demoLinePayload)は、架空の予定だけから送信用JSONを作り、実際のカレンダーやLINEへはアクセスしません。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

仕事効率化のおすすめ書籍

仕事用PCの選び方

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次