LINE BotとGoogle Apps Scriptで筋トレリマインダーを作ってみた
はじめに
筋トレを習慣化したかったので、LINEで毎日筋トレメニューを通知してくれるBotを作りました。
最初は「毎日メニューが届けば十分」くらいの軽い気持ちでしたが、作っているうちに以下の機能も欲しくなりました。
- LINEで今日のメニューを確認したい
- 疲れている日は短縮版を見たい
- 完了したら記録したい
- 週ごとに何回できたか振り返りたい
そこで今回は、LINE BotとGoogle Apps Scriptを使って、筋トレリマインダーBotを作成しました。
作ったもの
作成したのは、LINEで使える筋トレリマインダーBotです。
できることは以下です。
- 毎日決まった時間に筋トレメニューをLINE通知する
- 「今日」と送ると今日の筋トレメニューを返信する
- 「短縮」と送ると疲れている日用の短縮メニューを返信する
- 「完了」と送るとGoogleスプレッドシートに記録する
- 「短縮完了」と送ると短縮版として記録する
- 「休み」と送ると休養として記録する
- 日曜日に今週の実行回数と達成率を通知する
元の筋トレメニューは、週5回、木曜・日曜休み、疲れている日は短縮版ありという構成です。
通知のイメージは以下のような感じです。
🏋️ 今日の筋トレ
月曜日:胸・肩・腕
1. ダンベルフロアプレス
2. ダンベルショルダープレス
3. ゴムバンドチェストプレス
4. ダンベルサイドレイズ
5. ダンベルフレンチプレス
6. プランク
有酸素:早歩き15分
疲れている日は「短縮」と送ってください。
日曜日には、以下のような週間集計も届くようにしました。
📊 今週の実行状況
完了:3回
短縮完了:1回
休み:1回
合計:4 / 5回
達成率:80%
短縮版込みでかなり良いペースです。
使用技術
今回使った技術は以下です。
- LINE Messaging API
- Google Apps Script
- Googleスプレッドシート
サーバーは立てていません。
Google Apps Scriptを使うことで、定期実行、Webhook、スプレッドシート記録をまとめて実装しました。
全体構成
全体構成は以下のようなイメージです。
LINE
↓ メッセージ送信
LINE Messaging API
↓ Webhook
Google Apps Script
↓ 記録
Googleスプレッドシート
また、毎日の通知はGoogle Apps Scriptの時間主導型トリガーで実行しています。
Google Apps Script
↓ 毎日決まった時間に実行
LINE Messaging API
↓
LINEに筋トレメニューを通知
実装した機能
段階的に以下の順番で作りました。
第1段階:毎日LINEに筋トレメニューを通知する
第2段階:LINEで「今日」「短縮」「完了」に返信する
第3段階:スプレッドシートに筋トレ記録を保存する
第4段階:日曜日に今週の実行回数を通知する
最初から全部作ろうとすると大変なので、まずは「LINEに1回通知が届く」ことを目標にしました。
第1段階:LINEに毎日筋トレメニューを通知する
まずは、Google Apps ScriptからLINEにメッセージを送る処理を作りました。
LINE DevelopersでMessaging APIチャネルを作成し、チャネルアクセストークンを発行します。
Apps Script側では、以下のようにアクセストークンを定義します。
const CHANNEL_ACCESS_TOKEN = 'ここにチャネルアクセストークンを貼る';
実際にLINEへ通知する関数は以下です。
function sendWorkoutReminder() {
const today = new Date();
const day = today.getDay();
// 0:日曜, 1:月曜, 2:火曜, 3:水曜, 4:木曜, 5:金曜, 6:土曜
let message = getWorkoutMessage(day);
// 日曜日だけ、今週の実行回数を追加する
if (day === 0) {
message += '\n\n' + getWeeklySummaryMessage();
}
const url = 'https://api.line.me/v2/bot/message/broadcast';
const payload = {
messages: [
{
type: 'text',
text: message
}
]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
Logger.log(response.getContentText());
}
曜日ごとのメニューは、getWorkoutMessage() で返すようにしました。
function getWorkoutMessage(day) {
const messages = {
1: `🏋️ 今日の筋トレ
月曜日:胸・肩・腕
1. ダンベルフロアプレス
2. ダンベルショルダープレス
3. ゴムバンドチェストプレス
4. ダンベルサイドレイズ
5. ダンベルフレンチプレス
6. プランク
有酸素:早歩き15分
疲れている日は「短縮」と送ってください。`,
2: `🏋️ 今日の筋トレ
火曜日:背中・腕
1. ワンハンドダンベルロー
2. ゴムバンドローイング
3. ダンベルリアレイズ
4. ダンベルカール
5. ハンマーカール
6. デッドバグ
有酸素:早歩き10〜15分
疲れている日は「短縮」と送ってください。`,
3: `🏋️ 今日の筋トレ
水曜日:脚・お尻
1. ダンベルスクワット
2. ダンベルルーマニアンデッドリフト
3. ブルガリアンスクワット
4. ゴムバンドヒップアブダクション
5. カーフレイズ
6. レッグレイズ
有酸素:早歩き or 軽いジョグ10分
脚トレ後なので、有酸素は短めでOKです。`,
4: `🧘 今日は回復日
木曜日:休み or 軽いストレッチ
おすすめ:
・早歩き 20〜30分
・肩まわりストレッチ
・股関節ストレッチ
・軽い腹筋
疲れていたら完全休養でOK。
回復もトレーニングの一部です。`,
5: `🏋️ 今日の筋トレ
金曜日:上半身全体
1. ダンベルフロアプレス
2. ワンハンドダンベルロー
3. ダンベルショルダープレス
4. ゴムバンドフェイスプル
5. ダンベルカール
6. ダンベルキックバック
有酸素:早歩き15〜20分
疲れている日は「短縮」と送ってください。`,
6: `🏋️ 今日の筋トレ
土曜日:下半身+全身
1. ゴブレットスクワット
2. ダンベルランジ
3. ダンベルルーマニアンデッドリフト
4. ゴムバンドスクワット
5. マウンテンクライマー
6. サイドプランク
有酸素:余裕があれば早歩き10分
きつければ有酸素は省略してOKです。`,
0: `😴 今日は完全休養日
日曜日:休み
・筋トレなし
・有酸素なしでもOK
・軽い散歩やストレッチはOK
しっかり休んで、月曜日からまた再開しましょう。`
};
return messages[day];
}
これで、曜日ごとに違う筋トレメニューをLINEへ通知できるようになりました。
第2段階:LINEで「今日」「短縮」「完了」に返信する
次に、LINEでメッセージを送ったときにBotが返信するようにしました。
LINEからのWebhookを受け取るために、Apps Script側で doPost(e) を用意します。
function doPost(e) {
const json = JSON.parse(e.postData.contents);
const event = json.events[0];
if (!event || event.type !== 'message') {
return ContentService.createTextOutput('OK');
}
if (event.message.type !== 'text') {
replyMessage(event.replyToken, 'テキストで送ってください。');
return ContentService.createTextOutput('OK');
}
const userMessage = event.message.text.trim();
const today = new Date();
const day = today.getDay();
let replyText = '';
if (userMessage === '今日') {
replyText = getWorkoutMessage(day);
} else if (userMessage === '短縮') {
replyText = getShortWorkoutMessage(day);
} else if (userMessage === '完了') {
try {
recordWorkout(day, '完了', '通常');
replyText = getCompleteMessage(day) + '\n\nスプレッドシートに記録しました。';
} catch (error) {
replyText = '完了は受け取りましたが、記録でエラーが出ました。\n\nエラー内容:' + error.message;
}
} else if (userMessage === '短縮完了') {
try {
recordWorkout(day, '短縮完了', '疲れている日');
replyText = getCompleteMessage(day) + '\n\n短縮版として記録しました。';
} catch (error) {
replyText = '短縮完了は受け取りましたが、記録でエラーが出ました。\n\nエラー内容:' + error.message;
}
} else if (userMessage === '休み') {
try {
recordWorkout(day, '休み', '休養');
replyText = '休養として記録しました。\n\n休むのもトレーニングの一部です。';
} catch (error) {
replyText = '休みは受け取りましたが、記録でエラーが出ました。\n\nエラー内容:' + error.message;
}
} else if (userMessage === 'ヘルプ') {
replyText = `使える言葉はこちらです。
・今日
今日の筋トレメニューを表示
・短縮
疲れている日用の短縮メニューを表示
・完了
通常メニューを完了として記録
・短縮完了
短縮メニューを完了として記録
・休み
休養として記録`;
} else {
replyText = `すみません、まだその言葉には対応していません。
使える言葉:
・今日
・短縮
・完了
・短縮完了
・休み
・ヘルプ`;
}
replyMessage(event.replyToken, replyText);
return ContentService.createTextOutput('OK');
}
返信処理は以下の関数にまとめました。
function replyMessage(replyToken, text) {
const url = 'https://api.line.me/v2/bot/message/reply';
const payload = {
replyToken: replyToken,
messages: [
{
type: 'text',
text: text
}
]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
Logger.log(response.getContentText());
}
第3段階:スプレッドシートに筋トレ記録を保存する
次に、「完了」と送ったらGoogleスプレッドシートに記録するようにしました。
スプレッドシートには以下のような列を用意しました。
| 日付 | 曜日 | メニュー | 状態 | メモ |
|---|
Apps Script側には、スプレッドシートIDを定義します。
const SPREADSHEET_ID = 'ここにスプレッドシートIDを貼る';
注意点として、ここに貼るのはURL全体ではなく、スプレッドシートIDだけです。
例えばURLが以下の場合、
https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxxxxxxxx/edit
使うのはこの部分だけです。
xxxxxxxxxxxxxxxxxxxxxxxx
記録用の関数は以下です。
function recordWorkout(day, status, memo) {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getActiveSheet();
const now = new Date();
const weekdays = {
0: '日曜日',
1: '月曜日',
2: '火曜日',
3: '水曜日',
4: '木曜日',
5: '金曜日',
6: '土曜日'
};
const menuNames = {
1: '胸・肩・腕',
2: '背中・腕',
3: '脚・お尻',
4: '回復日',
5: '上半身全体',
6: '下半身+全身',
0: '完全休養日'
};
sheet.appendRow([
Utilities.formatDate(now, 'Asia/Tokyo', 'yyyy/MM/dd'),
weekdays[day],
menuNames[day],
status,
memo
]);
}
これで、LINEから以下のように送るだけで記録できます。
完了
通常メニューとして記録。
短縮完了
短縮版として記録。
休み
休養として記録。
スプレッドシートには以下のように記録されます。
| 日付 | 曜日 | メニュー | 状態 | メモ |
|---|---|---|---|---|
| 2026/05/07 | 木曜日 | 回復日 | 完了 | 通常 |
| 2026/05/08 | 金曜日 | 上半身全体 | 短縮完了 | 疲れている日 |
| 2026/05/09 | 土曜日 | 下半身+全身 | 休み | 休養 |
第4段階:日曜日に今週の実行回数を通知する
最後に、日曜日の通知に今週の実行回数を追加しました。
カウント対象は以下です。
- 完了
- 短縮完了
「休み」は実行回数には含めません。
週間集計用の関数は以下です。
function getWeeklySummaryMessage() {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getActiveSheet();
const values = sheet.getDataRange().getValues();
const today = new Date();
const startOfWeek = getStartOfWeekMonday(today);
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
endOfWeek.setHours(23, 59, 59, 999);
let completeCount = 0;
let shortCompleteCount = 0;
let restCount = 0;
for (let i = 1; i < values.length; i++) {
const row = values[i];
const dateValue = row[0];
const status = row[3];
if (!dateValue || !status) {
continue;
}
const recordDate = convertToDate(dateValue);
if (!recordDate) {
continue;
}
if (recordDate >= startOfWeek && recordDate <= endOfWeek) {
if (status === '完了') {
completeCount++;
} else if (status === '短縮完了') {
shortCompleteCount++;
} else if (status === '休み') {
restCount++;
}
}
}
const totalDone = completeCount + shortCompleteCount;
const weeklyTarget = 5;
const achievementRate = Math.round((totalDone / weeklyTarget) * 100);
let comment = '';
if (totalDone >= 5) {
comment = '週5回達成です。かなり良いです。';
} else if (totalDone >= 4) {
comment = '短縮版込みでかなり良いペースです。';
} else if (totalDone >= 3) {
comment = '習慣化としては十分いい流れです。';
} else if (totalDone >= 1) {
comment = '今週も記録できています。来週はまず3回を目標にしましょう。';
} else {
comment = '今週は未実施でした。来週は短縮版でもいいので1回から再開しましょう。';
}
return `📊 今週の実行状況
完了:${completeCount}回
短縮完了:${shortCompleteCount}回
休み:${restCount}回
合計:${totalDone} / ${weeklyTarget}回
達成率:${achievementRate}%
${comment}`;
}
週の開始日を月曜日にするため、以下の関数も用意しました。
function getStartOfWeekMonday(date) {
const result = new Date(date);
const day = result.getDay();
const diff = day === 0 ? -6 : 1 - day;
result.setDate(result.getDate() + diff);
result.setHours(0, 0, 0, 0);
return result;
}
スプレッドシートの日付をDate型として扱うため、以下の関数も追加しました。
function convertToDate(value) {
if (value instanceof Date) {
return value;
}
if (typeof value === 'string') {
const parts = value.split('/');
if (parts.length === 3) {
const year = Number(parts[0]);
const month = Number(parts[1]) - 1;
const day = Number(parts[2]);
return new Date(year, month, day);
}
}
return null;
}
これで、日曜日の通知にだけ週間集計を追加できるようになりました。
テスト用関数
日曜日まで待たずに確認するため、テスト用の関数も作りました。
function testSundayReminder() {
let message = getWorkoutMessage(0);
message += '\n\n' + getWeeklySummaryMessage();
const url = 'https://api.line.me/v2/bot/message/broadcast';
const payload = {
messages: [
{
type: 'text',
text: message
}
]
};
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
Logger.log(response.getContentText());
}
この関数を実行すると、日曜日と同じ形式のメッセージがLINEに届きます。
つまずいたポイント
今回つまずいたポイントはいくつかありました。
チャネルアクセストークンの場所がわかりにくい
最初、チャネルシークレットやアサーション署名キーは見つかったのですが、チャネルアクセストークンの場所がわかりにくかったです。
今回使うのはチャネルシークレットではなく、Messaging APIのチャネルアクセストークンです。
const CHANNEL_ACCESS_TOKEN = 'ここにチャネルアクセストークンを貼る';
また、コード側で以下のように Bearer を付けているため、トークンの値に Bearer は含めません。
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
認証エラー
最初に以下のようなエラーが出ました。
{
"message": "Authentication failed. Confirm that the access token in the authorization header is valid."
}
原因はアクセストークン周りでした。
確認したことは以下です。
- 正しいMessaging APIチャネルのアクセストークンか
- チャネルシークレットを貼っていないか
-
Bearerを二重に付けていないか - 余計な空白や改行が入っていないか
GASは保存だけでは反映されないことがある
Webhookで使っているWebアプリは、コードを保存しただけでは反映されないことがあります。
そのため、コードを変更したら以下の操作をしました。
デプロイ
↓
デプロイを管理
↓
既存デプロイを編集
↓
新バージョン
↓
デプロイ
スプレッドシートIDの貼り間違い
SPREADSHEET_ID にはURL全体ではなく、ID部分だけを貼る必要があります。
NG例:
const SPREADSHEET_ID = 'https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxx/edit';
OK例:
const SPREADSHEET_ID = 'xxxxxxxxxxxxxxxx';
SpreadsheetAppの権限許可
SpreadsheetApp を使うためには、Google側の権限許可が必要でした。
LINEから実行しても権限確認が出ないため、一度Apps Script上でテスト関数を手動実行して許可しました。
function testRecordWorkout() {
const today = new Date();
const day = today.getDay();
recordWorkout(day, 'テスト', '権限確認');
}
改善できそうなところ
今回は最低限の筋トレリマインダーとして作りましたが、まだ改善できそうな点があります。
種目ごとの重量・回数を記録する
今は「完了」「短縮完了」だけを記録しています。
今後は以下も記録できるようにしたいです。
- 重量
- 回数
- セット数
- 疲労度
- やる気
「今週」と送ったら集計を返す
日曜日の自動通知だけでなく、LINEで以下のように送ったら集計を返せるようにしても便利そうです。
今週
メニューのやり方を返す
例えば、以下のように種目名を送るとやり方を返す機能も追加できそうです。
スクワット
フロアプレス
AI相談機能を追加する
将来的には、OpenAI APIなどと組み合わせて、以下のような入力に対してメニュー調整を返せるようにしても面白そうです。
今日は疲れている
肩が痛い
20分しかない
まとめ
今回は、LINE BotとGoogle Apps Scriptを使って、筋トレリマインダーBotを作りました。
できるようになったことは以下です。
- 毎日LINEに筋トレメニューを通知する
- 「今日」で今日のメニューを確認する
- 「短縮」で短縮版メニューを確認する
- 「完了」でスプレッドシートに記録する
- 「短縮完了」で短縮版として記録する
- 「休み」で休養として記録する
- 日曜日に今週の実行回数と達成率を通知する
LINE BotとGoogle Apps Scriptだけでも、個人用の習慣化ツールとしてはかなり実用的なものが作れると感じました。
筋トレ以外にも、以下のような用途に応用できそうです。
- 勉強記録
- 読書記録
- 日記
- 体重管理
- 水分補給リマインダー
- 資格学習の進捗管理
小さく作り始めて、少しずつ機能追加していくと、挫折しにくくてよかったです。