複数チャンネルにメッセージを一斉送信するGAS×Slackボットを作った話
この記事は NE Advent Calendar 2025 の3日目の記事です。
こんにちわ。
ロカルコ事業部でマネージャーをしている@yaginuuuです。
やりたいこと
弊社の自治体向けふるさと納税支援事業では、クライアントや返礼品事業者、パートナー企業ごとに Slack の専用チャンネルを用意し、報告相談等を実施しています。このようなケースは多いと思います。
本記事では、そうした複数チャンネルに対して「その都度考えた文面を、一斉送信するための仕組み」を Google スプレッドシートと Google Apps Script(GAS)、Slack Bot トークンで実装する方法を紹介します。
背景・課題
- クライアント / 返礼品事業者 / パートナーのSlackチャンネルが数十個
- 全社MTG、メンテ告知、障害連絡などで「同じメッセージを複数グループに送る」機会が多い
- 毎回複数のチャンネルに手動コピペは非効率
- 自動化は不要(文面はその都度考えるため)
- 誤爆防止策必須
要件
✅ スプレッドシートで送信先(チャンネル)と種別を管理
✅ その都度、文面をシートに入力して送信
✅ カスタムメニューから「全件」「自治体だけ」「事業者だけ」「パートナーだけ」を選択
✅ 送信前に「プレビュー確認」+「最終確認ダイアログ」
✅ 1つのSlack Botトークンで複数チャンネルに送信
スプレッドシートの構成
channels シート(送信先マスタ)

A1: role_filter → 「自治体」or「返礼品事業者」or「パートナー」or空
B1: send_to_all → TRUE(全種別)/ FALSE(A1指定)
A3: 本文 → 「今週金曜は弊社側の全社MTGが...」
message シート(その都度の文面)
運用例:
A1: 自治体 B1: FALSE A3: [当日の案内文]
→ 自治体だけに送信
Slack Botトークンの準備
- SlackでApp作成 → Bot User追加
- スコープ:chat:write
- 各チャンネルにBot招待
- トークン(xoxb-...)をGAS「スクリプトプロパティ」に保存
キー:SLACK_BOT_TOKEN
実装コード
- カスタムメニュー
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Slack通知')
.addItem('送信対象をプレビュー', 'previewTargets')
.addSeparator()
.addItem('全件送信', 'sendAll')
.addSeparator()
.addItem('自治体向けを送信', 'sendToJichitai')
.addItem('返礼品事業者向けを送信', 'sendToVendor')
.addItem('パートナー向けを送信', 'sendToPartner')
.addToUi();
}
- エントリーポイント
function sendAll() {
const message = getMessageBody_();
if (!message) return;
const rows = getChannelsRows_();
notifyToSlack_(rows, message);
}
function sendToJichitai() { sendByRole_('自治体'); }
function sendToVendor() { sendByRole_('返礼品事業者'); }
function sendToPartner() { sendByRole_('パートナー'); }
function sendByRole_(roleType) {
const message = getMessageBody_();
if (!message) return;
const rows = getChannelsRows_(roleType);
notifyToSlack_(rows, message);
}
- messageシート読み取り
function getMessageBody_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('message');
if (!sheet) {
SpreadsheetApp.getUi().alert('message シートが見つかりません。');
return '';
}
const body = sheet.getRange('A3').getValue();
if (!body) {
SpreadsheetApp.getUi().alert('message シートの A3 に本文を入力してください。');
return '';
}
return body;
}
- channelsシート読み取り
function getChannelsRows_(roleType) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('channels');
if (!sheet) {
SpreadsheetApp.getUi().alert('channels シートが見つかりません。');
return [];
}
const lastRow = sheet.getLastRow();
if (lastRow < 2) return [];
const values = sheet.getRange(2, 1, lastRow - 1, 6).getValues();
return values
.filter(row => row[5] && (!roleType || row[0] === roleType))
.map(row => ({
roleType: row[0], name: row[1], channelId: row[2],
mentionType: row[3], mentionTarget: row[4]
}));
}
- プレビュー機能
function previewTargets() {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const msgSheet = ss.getSheetByName('message');
if (!msgSheet) { ui.alert('message シートが見つかりません。'); return; }
const roleFilter = msgSheet.getRange('A1').getValue();
const sendToAll = msgSheet.getRange('B1').getValue();
let rows = sendToAll ? getChannelsRows_() : (roleFilter ? getChannelsRows_(roleFilter) : []);
if (!rows.length) {
ui.alert('現在の設定では送信対象のチャンネルがありません。');
return;
}
const lines = rows.map((row, i) => `${i+1}. [${row.roleType}] ${row.name} (${row.channelId})`);
ui.alert('送信対象チャンネル(' + rows.length + '件):\n\n' + lines.join('\n'));
}
- 送信+確認ダイアログ
function notifyToSlack_(rows, message) {
const ui = SpreadsheetApp.getUi();
if (!rows.length) { ui.alert('送信対象のチャンネルがありません。'); return; }
const token = PropertiesService.getScriptProperties().getProperty('SLACK_BOT_TOKEN');
if (!token) { ui.alert('スクリプトプロパティに SLACK_BOT_TOKEN が設定されていません。'); return; }
const previewLines = rows.slice(0, 10).map((row, i) => `${i+1}. [${row.roleType}] ${row.name} (${row.channelId})`);
let confirmText = `以下の ${rows.length}件に送信:\n\n${previewLines.join('\n')}`;
if (rows.length > 10) confirmText += `\n…ほか ${rows.length-10}件`;
confirmText += '\n\nよろしいですか?';
if (ui.alert('Slack送信の確認', confirmText, ui.ButtonSet.OK_CANCEL) !== ui.Button.OK) {
ui.alert('送信をキャンセルしました。');
return;
}
let success = 0, fail = 0;
rows.forEach(row => {
const mention = buildMentionText_(row.mentionType, row.mentionTarget);
const text = mention ? `${mention}\n${message}` : message;
if (postToSlack_(token, row.channelId, text)) success++; else fail++;
});
ui.alert(`送信完了: ${success}件 / 失敗: ${fail}件`);
}
- メンション生成+Slack API
function buildMentionText_(mentionType, mentionTarget) {
if (!mentionType) return '';
if (mentionType === 'here') return '<!here>';
if (mentionType === 'channel') return '<!channel>';
if (mentionType === 'user' && mentionTarget) {
return String(mentionTarget).split(',').map(s => s.trim()).filter(s => s)
.map(id => `<@${id}>`).join(' ');
}
return '';
}
function postToSlack_(token, channelId, text) {
const url = 'https://slack.com/api/chat.postMessage';
const payload = { channel: channelId, text };
const params = {
method: 'post',
contentType: 'application/json; charset=utf-8',
headers: { Authorization: 'Bearer ' + token },
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const res = UrlFetchApp.fetch(url, params);
if (res.getResponseCode() !== 200) {
console.error('Slack API error: ' + res.getContentText());
return false;
}
return JSON.parse(res.getContentText()).ok;
}
運用フロー
-
messageシート編集
A1: 「自治体」 B1: FALSE A3: [案内文] -
メニュー「Slack通知 → 送信対象をプレビュー」
→ 「1. [自治体] ○○市役所...」一覧確認 -
「Slack通知 → 自治体向けを送信」
→ 最終確認ダイアログ → OK → 一斉送信完了!
運用してみて
✅ 手動コピペ数十回 → メニュー1クリックに短縮
✅ プレビュー+確認で誤爆ゼロ
✅ 文面は都度自由編集(テンプレ化不要)
✅ enabled列で一時停止も簡単
✅ GAS+スプレッドシートでゼロコスト
サクッと作れて、みんなハッピーなので、やってみてください。
Discussion