0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GAS × LINE × Gemini】遠く離れて暮らす母のために、お薬飲んだよbotを作った話(401エラーとの闘いを超えて)

0
Last updated at Posted at 2026-09-12

🌸 はじめに

私は現在千葉県に住んでおり、京都で暮らしをしている高齢の母がいます。
離れて暮らしていると、毎日のお薬の飲み忘れや体調がどうしても心配になります。

そこで、母が負担なく楽しく使えて、自分も安心できる**「LINEでのお薬飲み忘れ防止&見守り自動化システム」**を、Google Apps Script(GAS)・LINE Messaging API・Gemini APIを使って作ってみることにしました!

一筋縄ではいかないトラブル(特にLINEからの 401 Unauthorized エラー…!)もありましたが、無事に完成して毎日元気に動いています。

同じように「遠く離れた親御さんの見守りシステムを作りたい!」と考えている方の参考になれば幸いです。


[母(京都)]
  ├─ ① 8時/12時/19時: 定期配信「お薬飲んだ?体調どう?」
  │
  ├─ ② ボタン送信「お薬のんだよ」
  │    ├─────────────────────────────┐
  ▼    ▼                             ▼
[GAS (Webhook)]               [子供(千葉)]
  │                                  ▲
  ├─ ③ Gemini API                    │
  │    (脳トレクイズを自動生成)        │
  ▼                                  │
[母のLINEへ返信]                     └─ ④ 即座にPush通知
                                         「お母様が薬を飲みました」

💡 完成したシステムの仕組み

  • 1日3回の自動定期リマインド(8:00 / 12:00 / 19:00)
  • Canvaで作った手作りボタン(母が見やすく押しやすい画像デザイン)
  • ワンタップの簡単操作(母は「お薬のんだよ」ボタンを押すだけ)
  • 子供へのリアルタイム通知(「飲んだ時刻」が千葉の自分に届く安心感)
  • Gemini APIによる日替わり脳トレクイズ(母が飽きずに続けられる工夫)

🎨 母のためのデザイン工夫:Canvaでボタン作成

高齢の母に毎日気持ちよく使ってもらうため、LINEの画面デザイン(リッチメニュー)にもこだわりました。

文字だけのボタンだと見づらかったり冷たい印象を与えてしまうため、Canvaを使って母が親しみやすいシンプルなボタンを自作しました!

  • 見やすい配色と大きめの文字:「お薬のんだよ」の文字を大きく、温かみのある色合いに設定
  • 直感的にわかるイラスト:文字を読むのが億劫な時でも、パッと見てすぐ押せるデザインに

「母への使いやすさ」を追求したことで、より愛着を持って使ってもらえるボットになりました。


📱 LINE公式アカウントの初期設定

LINE Botを運用するための初期設定手順です。

1. LINE Business IDの作成と公式アカウント開設

  1. LINE Official Account Manager にアクセスし、アカウントを開設します。
  2. アカウント名(例:「おかあさんのお薬リマインダー」など)を設定します。

2. Messaging APIの有効化

  1. 管理画面の右上「設定」➔ 「Messaging API」 を選択します。
  2. 「Messaging APIを利用する」 をクリックし、プロバイダーを作成・選択します。
  3. LINE Developers に移動し、発行された 「チャネルアクセストークン(長期)」 を取得・保管しておきます。

3. 応答設定の変更

  1. LINE Official Account Managerの 「応答設定」 を開きます。
  2. 応答モードBot に設定します。
  3. あいさつメッセージ / 自動応答メッセージ:オフ(または非表示)に設定し、GAS側のプログラムのみで応答するようにします。
  4. Webhookオン に設定します。

🛠️ 開発過程で直面した最大の壁:401 Unauthorized との闘い

開発中、LINE Developersの「検証」ボタンを押すたびに以下のエラーが出続けてドハマりしました……。

⚠️ ボットサーバーから200以外のHTTPステータスコードが返されました。(401 Unauthorized)

この解決には2つの大きな落とし穴がありました。

1. GASのデプロイ時の権限設定

GASをWebアプリとしてデプロイする際、「アクセスできるユーザー」を**「全員(Anyone)」**にしておかないと、LINEプラットフォームからのアクセスをGoogle側が遮断してしまい401エラーになります。

2. LINEの「検証」ボタンの挙動対策

LINE Developersの「検証」ボタンを押すと、LINEからダミーのテストデータ(replyToken000... など)が送られてきます。
プログラム内でそのままLINEのAPIを叩こうとすると認証失敗し、結果としてGAS全体が401エラーを返したように判定されてしまいます。

🔑 解決コード

テストデータが届いた場合はLINE APIを叩かずに即座に 200 OK を返して処理を抜ける記述を入れることで、検証エラーを100%回避できました。

// LINE Developersの「検証」ボタン対策
if (!json.events || json.events.length === 0) {
  return successOutput;
}
const event = json.events[0];
if (event.replyToken === "00000000000000000000000000000000" || event.replyToken === "ffffffffffffffffffffffffffffffff") {
  return successOutput;
}

💻 実装したコード(全コード)

Google Apps Script(GAS)に貼り付けてそのまま使えるフルコードです。

// ==========================================
// 設定情報(※ご自身のキー・IDに差し替えてください)
// ==========================================
const GEMINI_API_KEY       = "YOUR_GEMINI_API_KEY";
const LINE_ACCESS_TOKEN     = "YOUR_LINE_CHANNEL_ACCESS_TOKEN";

// LINEユーザーID
const FAMILY_LINE_USER_ID = "YOUR_FAMILY_LINE_USER_ID"; // 通知を受ける家族(自分)のID
const MOTHER_LINE_USER_ID = "YOUR_MOTHER_LINE_USER_ID"; // お母様のID

// ==========================================
// Webhook受信処理(LINEからのメッセージ受信)
// ==========================================
function doPost(e) {
  const successOutput = ContentService.createTextOutput(JSON.stringify({ content: 'ok' }))
    .setMimeType(ContentService.MimeType.JSON);

  try {
    if (!e || !e.postData || !e.postData.contents) {
      return successOutput;
    }

    const payloadData = JSON.parse(e.postData.contents);
    if (!payloadData.events || payloadData.events.length === 0) {
      return successOutput;
    }

    const event = payloadData.events[0];
    const replyToken = event.replyToken;

    // 検証ボタン対策
    if (replyToken === "00000000000000000000000000000000" || replyToken === "ffffffffffffffffffffffffffffffff") {
      return successOutput;
    }

    // 送信者のLINE User IDをログに出力(ID特定用)
    if (event.source && event.source.userId) {
      Logger.log("送信者のLINE User ID: " + event.source.userId);
    }

    // メッセージ受信処理
    if (event.type === 'message' && event.message && event.message.type === 'text') {
      const userText = event.message.text.trim();
      
      if (userText === "お薬のんだよ" || userText === "今日は薬なし") {
        // 1. 家族へ通知
        sendPushToFamily(userText);
        // 2. お母様へ毎回内容の異なるクイズまたはメッセージを返信
        const isQuiz = Math.random() < 0.5;
        const replyMessage = generateGeminiContent(isQuiz);
        replyToUser(replyToken, replyMessage);
      }
    }
  } catch (error) {
    Logger.log("doPost Error: " + error.toString());
  }
  
  return successOutput;
}

// ==========================================
// 定時配信処理(8時・12時・19時に自動実行する関数)
// ==========================================
function sendDailyReminder() {
  const url = 'https://api.line.me/v2/bot/message/push';
  const messageText = "【定期連絡 🌸】\nお薬は飲みましたか?体調はどうですか?\n元気なら「お薬のんだよ」ボタンを押して教えてね!";

  const payload = {
    "to": MOTHER_LINE_USER_ID,
    "messages": [{ "type": "text", "text": messageText }]
  };

  const options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    },
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };

  UrlFetchApp.fetch(url, options);
}

// ==========================================
// Gemini API 呼び出し(多様なクイズの動的生成)
// ==========================================
function generateGeminiContent(isQuiz) {
  const fallbackQuizzes = [
    "【今日の脳トレクイズ 🧠】\n秋に山で拾うことができる、茶色くて固い木の実といえば何でしょう?\n① どんぐり\n② いちご\n③ メロン\n\n正解は①どんぐりです🌰 季節の深まりを感じますね。",
    "【今日の脳トレクイズ 🧠】\n日本で一番高い山といえば何山でしょう?\n① 富士山\n② 高尾山\n③ 六甲山\n\n正解は①富士山です🗻 今日も健康で元気に過ごしましょうね。"
  ];
  const fallbackMessages = [
    "【今日のお守りことば ✨】\n「笑顔は心のお薬」です。今日も好きなことをして、ゆったり過ごしてくださいね😊",
    "【今日のお守りことば ✨】\n「焦らず、ゆっくり、自分のペースで」\n今日も無理をせず穏やかな一日をお過ごしください🌸"
  ];

  const randomIndex = Math.floor(Math.random() * fallbackQuizzes.length);
  const defaultQuiz = fallbackQuizzes[randomIndex];
  const defaultMsg  = fallbackMessages[randomIndex];

  try {
    const url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=' + GEMINI_API_KEY;
    
    // クイズテーマをランダム抽出してプロンプトの多様性を確保
    const quizThemes = ["季節の行事や花", "日本の都道府県や特産品", "ことわざ・慣用句", "昭和の懐かしい思い出", "昔ながらの知恵・生活習慣"];
    const selectedTheme = quizThemes[Math.floor(Math.random() * quizThemes.length)];

    let promptText = "";
    if (isQuiz) {
      promptText = "高齢のお母様向けの「今日の脳トレクイズ 🧠」を1つ作ってください。今回のテーマは【" + selectedTheme + "】です。テーマに沿った3択問題を1つ作成し、正解と温かい解説を含めて全体で200文字程度の読みやすい文章にしてください。";
    } else {
      promptText = "高齢のお母様向けの「今日のお守りことば ✨」を1つ作ってください。前向きになれる言葉や労いの言葉を添え、全体で150文字程度の温かい文章にしてください。";
    }

    const payload = { "contents": [{ "parts": [{ "text": promptText }] }] };
    const options = {
      "method": "post",
      "contentType": "application/json",
      "payload": JSON.stringify(payload),
      "muteHttpExceptions": true
    };

    const response = UrlFetchApp.fetch(url, options);
    if (response.getResponseCode() === 200) {
      const resData = JSON.parse(response.getContentText());
      if (resData.candidates && resData.candidates[0] && resData.candidates[0].content && resData.candidates[0].content.parts[0].text) {
        return resData.candidates[0].content.parts[0].text.trim();
      }
    }
    return isQuiz ? defaultQuiz : defaultMsg;
  } catch (err) {
    return isQuiz ? defaultQuiz : defaultMsg;
  }
}

// ==========================================
// LINE返信処理
// ==========================================
function replyToUser(replyToken, textMessage) {
  const url = 'https://api.line.me/v2/bot/message/reply';
  const payload = {
    "replyToken": replyToken,
    "messages": [{ "text": textMessage, "type": "text" }]
  };
  const options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    },
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };
  UrlFetchApp.fetch(url, options);
}

// ==========================================
// 家族への通知処理
// ==========================================
function sendPushToFamily(userText) {
  const url = 'https://api.line.me/v2/bot/message/push';
  const now = Utilities.formatDate(new Date(), "Asia/Tokyo", "HH:mm");
  const notificationMessage = "お母様が「" + userText + "」を選択・送信しました。(時刻: " + now + "";

  const payload = {
    "to": FAMILY_LINE_USER_ID,
    "messages": [{ "type": "text", "text": notificationMessage }]
  };

  const options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    },
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };

  UrlFetchApp.fetch(url, options);
}

🍀 心がけたポイント(母への思いやり)

  1. Canvaでのデザイン手作り
    • 高齢の母でも押し間違いのないよう、大きなボタンと見やすい可愛いイラストを制作して配置しました。
  2. 毎回違うクイズが出ること
    • 同じクイズだと単なる「作業」になってしまうため、Gemini APIへのプロンプトに「都道府県」「昭和の思い出」「ことわざ」などのランダムテーマを注入し、毎日ワクワクしてもらえるように工夫しました。
  3. 迷わず押せるUI設定
    • リッチメニューに配置し、母が文字入力をしなくてもワンタップで完結するように調整しました。
      スクリーンショット 2026-09-13 012438.jpg
      スクリーンショット 2026-09-13 012402.jpg

🎈 おわりに

千葉と京都という物理的な距離はありますが、毎日の「お薬のんだよ」の通知がスマホに届くたびに、**「今日も元気に過ごしているな」**と心からホッとできるようになりました。

母も「毎日の脳トレクイズとお守りことばが楽しみ!」と喜んで使ってくれています。

GAS・LINE API・Canvaの組み合わせは、個人利用であれば完全無料で構築できます。
遠く離れたご家族の見守りに悩んでいる方は、ぜひチャレンジしてみてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?