2
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?

LINEグループの一斉送信が面倒すぎたので、GASで一斉送信Botを作った話

Last updated at Posted at 2025-06-25

こんにちは。tampopo256です。
普段は教育系のWebサービスの開発をしていました。(過去形)

🏁 はじめに 〜サークル運営で直面したLINE連絡地獄〜

僕は大学で大会運営を担当するサークルに所属しています。
大会準備では、広報チーム・会場チーム・受付チームなど、チームごとにLINEグループを作って運営しているのですが…

ある時、全チームに「明日の集合時間」などの連絡を一斉に送りたいシーンがありました。

そのときの現場の様子がこちら:

A「全チームに送った?」
Me「たぶん送ったけど、何チームに送ったか忘れた」
A「誰がどこに送ったか確認できる?」
Me「……できません……😇」

はい、めちゃくちゃ非効率です。
しかも大会直前の慌ただしい中で、「送信ミス」や「連絡漏れ」は命取り。

そこで、

LINEグループ間で一斉送信できて、誰がいつどこに何を送ったかもログとして残せるBotをGAS(Google Apps Script)で開発しました。

🔧 使用技術と前提条件

技術 用途
Google Apps Script (GAS) Botの処理・ログ記録
LINE Messaging API グループへのPush送信
Google Spreadsheet グループ管理・ログ保存

前提条件:

  • Botを各グループに招待済みであること
  • 送信元グループと送信先グループを /register-source, /register-target コマンドで登録しておくこと

🌀 どうやって動いてるのか?

グループ間の一斉送信

  • 送信元グループでメッセージを送る
  • Botが事前に登録された送信先グループすべてに一斉送信
  • 同時にスプレッドシートにログを記録

ログに記録される内容

ログ項目 内容例
日時 2025/06/25 15:30:21
送信者(userId) Uxxxxxxxxxxxxx
送信元グループ グループ(C12345…)
メッセージ本文 集合時間は10:00です!
送信先グループ 広報チーム, 受付チーム

🧾 スプレッドシート構成

シート名 役割
SourceGroups 送信元グループのIDと名前
TargetGroups 送信先グループのIDと名前
Logs メッセージ送信ログ

✅ GASスクリプト

こちら
const CHANNEL_ACCESS_TOKEN = 'YOUR_LINE_CHANNEL_ACCESS_TOKEN';
const SHEET_ID = 'YOUR_SPREADSHEET_ID';

function doPost(e) {
  const json = JSON.parse(e.postData.contents);
  const event = json.events[0];
  if (!event || event.type !== 'message' || event.message.type !== 'text') return;

  const text = event.message.text;
  const sourceGroupId = event.source.groupId;
  const sourceGroupName = `グループ(${sourceGroupId.slice(0, 6)}...)`;
  const senderName = event.source.userId || '不明';

  if (text === '/register-source') {
    registerGroup(sourceGroupId, sourceGroupName, 'SourceGroups');
    replyToGroup(event.replyToken, `✅ このグループを送信元に登録しました。`);
    return;
  }

  if (text === '/register-target') {
    registerGroup(sourceGroupId, sourceGroupName, 'TargetGroups');
    replyToGroup(event.replyToken, `✅ このグループを送信先に登録しました。`);
    return;
  }

  if (!isGroupRegistered(sourceGroupId, 'SourceGroups')) return;

  const targetGroups = getGroups('TargetGroups');
  const results = [];

  targetGroups.forEach(group => {
    const success = pushMessageToGroup(group.id, text);
    results.push({ name: group.name, id: group.id, success });
  });

  const sentGroups = results.filter(r => r.success);
  const resultText =
    `✅ 一斉送信完了\n送信先:${sentGroups.length}グループ\n` +
    sentGroups.map(r => `・${r.name}`).join('\n');

  replyToGroup(event.replyToken, resultText);

  logSendEvent({
    timestamp: new Date(),
    senderName,
    sourceGroupId,
    sourceGroupName,
    message: text,
    targets: sentGroups.map(g => g.name)
  });
}

function registerGroup(groupId, groupName, sheetName) {
  const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(sheetName);
  const values = sheet.getDataRange().getValues();
  const alreadyRegistered = values.some(row => row[0] === groupId);
  if (!alreadyRegistered) {
    sheet.appendRow([groupId, groupName]);
  }
}

function isGroupRegistered(groupId, sheetName) {
  const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(sheetName);
  const values = sheet.getDataRange().getValues();
  return values.some(row => row[0] === groupId);
}

function getGroups(sheetName) {
  const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(sheetName);
  const values = sheet.getDataRange().getValues();
  return values.map(row => ({ id: row[0], name: row[1] }));
}

function pushMessageToGroup(groupId, message) {
  const url = 'https://api.line.me/v2/bot/message/push';
  const payload = {
    to: groupId,
    messages: [{ type: 'text', text: message }],
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: `Bearer ${CHANNEL_ACCESS_TOKEN}` },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };

  const res = UrlFetchApp.fetch(url, options);
  return res.getResponseCode() === 200;
}

function replyToGroup(replyToken, message) {
  const url = 'https://api.line.me/v2/bot/message/reply';
  const payload = {
    replyToken: replyToken,
    messages: [{ type: 'text', text: message }],
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: `Bearer ${CHANNEL_ACCESS_TOKEN}` },
    payload: JSON.stringify(payload),
  };

  UrlFetchApp.fetch(url, options);
}

function logSendEvent(log) {
  const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName('Logs');
  sheet.appendRow([
    Utilities.formatDate(log.timestamp, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'),
    log.senderName,
    log.sourceGroupName,
    log.message,
    log.targets.join(', ')
  ]);
}

⚠️ 注意点:月200件制限あり(重要)

LINEのMessaging API(無料プラン)には以下の制限があります:

  • 月間のPushメッセージ送信数は200件まで(無料枠)
  • 送信先グループが複数の場合、それぞれ1件としてカウントされる
    → 例:5グループに同じメッセージ送信 = 5件としてカウント

つまり、月に40回程度しか5グループへの一斉送信はできません

解決策(必要であれば)

  • 有料プランへのアップグレード
  • メールやSlackへの切り替え
  • LINE通知は重要連絡のみに限定し、通常連絡は別媒体に分離

💡 実際使って感じたメリット

  • 全体通達の**「送ったつもり」事故がゼロに**
  • チーム間連携の情報伝達が高速化
  • 運営メンバー間での役割明確化とトラブル回避

✅ 学びまとめ

  • サークル運営でも業務改善Botは大活躍する
  • GAS × LINE Bot の相性は抜群
  • ログを残すだけで「責任の見える化」ができる
  • 表示名取得には制約あり(OAuth必須)
  • LINEの無料枠では月200件の送信制限に注意!

📣 この仕組み、他のサークルやゼミでも使えるはず!
「こういう機能追加したい!」などあればぜひコメントで教えてください!

2
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
2
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?