0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者OK】Slackで放置された質問を自動検出して通知するBotを作る【GASで無料&サーバーレス】

0
Last updated at Posted at 2026-01-14

Slackのチャンネルで、ユーザーの質問が放置されてしまうことってありませんか?
「誰か答えてあげて…」と思いながら流れていくスレッド。

そんな 「返事が来てない質問スレッド」を自動検出して、情シスに通知するSlack Bot を作ります。

しかも、コードはコピペOK。図解付き。初心者でも動く。
今回は Google Apps Script(GAS)で無料&サーバーレスで実現 します。


✅ やること

  • 特定のSlackチャンネルを定期監視
  • スレッドが放置されてるかどうかを判定
  • 解決済みならスキップ
  • 一定時間返事がなければ情シスメンバーにアラート
  • スパム防止のためクールダウン時間も設定

🛠 必要なもの

  • Googleアカウント(Gmailとか使ってればOK)
  • Slackワークスペースの管理者権限(Botを作成・トークン発行のため)
  • SlackのBotトークン(後述)
  • GAS(Google Apps Script)の基本操作(貼り付けと実行だけでOK)

📦 スクリプト全体(コピペOK)

// ==========================================
// Slackの放置スレッド監視Bot(GAS)
// ==========================================
// チャンネルを監視して、一定時間返信がなければ管理者にアラート送信
// ✅リアクションが付いた投稿は「解決済み」とみなして通知しない
// ==========================================

const CONFIG = {
  TARGET_CHANNEL_ID: 'C1234567890', // ★ここに監視したいチャンネルIDを入れる
  ALERT_CHANNEL_ID: 'C0987654321', // ★ここに通知先チャンネルIDを入れる
  ADMIN_USER_IDS: ['U111111', 'U222222'], // ★情シスメンバーのIDリスト
  THRESHOLD_MINUTES: 60,
  COOLDOWN_HOURS: 4,
  RESOLVED_REACTION_NAME: 'white_check_mark',
  MENTION_ADMINS: true
};

function checkNeglectedThreads() {
  const token = PropertiesService.getScriptProperties().getProperty('SLACK_TOKEN');
  if (!token) throw new Error('"SLACK_TOKEN" が設定されていません。');

  const history = fetchChannelHistory(token, CONFIG.TARGET_CHANNEL_ID, 20);
  if (!history || history.length === 0) return;

  const now = Math.floor(Date.now() / 1000);
  const thresholdSeconds = CONFIG.THRESHOLD_MINUTES * 60;
  const cooldownSeconds = CONFIG.COOLDOWN_HOURS * 3600;
  const scriptProps = PropertiesService.getScriptProperties();

  history.forEach(msg => {
    if (msg.subtype === 'bot_message' || msg.subtype === 'channel_join') return;
    if (hasResolvedReaction(msg)) {
      const cacheKey = 'alerted_' + msg.ts;
      if (scriptProps.getProperty(cacheKey)) scriptProps.deleteProperty(cacheKey);
      return;
    }

    let lastUser = msg.user;
    let lastTs = msg.ts;
    let threadTs = msg.ts;

    if (msg.thread_ts && msg.reply_count > 0) {
      const replies = fetchThreadReplies(token, CONFIG.TARGET_CHANNEL_ID, msg.thread_ts);
      if (replies && replies.length > 0) {
        const lastReply = replies[replies.length - 1];
        lastUser = lastReply.user;
        lastTs = lastReply.ts;
        threadTs = msg.thread_ts;
      }
    }

    const isAdmin = CONFIG.ADMIN_USER_IDS.includes(lastUser);
    const elapsed = now - parseFloat(lastTs);
    if (!isAdmin && elapsed > thresholdSeconds) {
      const cacheKey = 'alerted_' + threadTs;
      const lastAlertedTime = scriptProps.getProperty(cacheKey);
      if (lastAlertedTime && (now - parseInt(lastAlertedTime)) < cooldownSeconds) return;

      const elapsedMinutes = Math.floor(elapsed / 60);
      sendAlert(token, threadTs, elapsedMinutes);
      scriptProps.setProperty(cacheKey, now.toString());
    }
  });
}

function fetchChannelHistory(token, channelId, limit) {
  const url = `https://slack.com/api/conversations.history?channel=${channelId}&limit=${limit}`;
  return callSlackApi(token, url).messages || [];
}

function fetchThreadReplies(token, channelId, ts) {
  const url = `https://slack.com/api/conversations.replies?channel=${channelId}&ts=${ts}`;
  return callSlackApi(token, url).messages || [];
}

function sendAlert(token, ts, minutesElapsed) {
  const permalink = getPermalink(token, CONFIG.TARGET_CHANNEL_ID, ts);
  let text = `⚠️ *放置検知アラート* ⚠️\nユーザーの最後の発言から *${minutesElapsed}分* が経過しています。\n対応漏れはありませんか?\n${permalink}`;

  if (CONFIG.MENTION_ADMINS) {
    const mentions = CONFIG.ADMIN_USER_IDS.map(id => `<@${id}>`).join(' ');
    text = `${mentions}\n${text}`;
  }

  const payload = {
    channel: CONFIG.ALERT_CHANNEL_ID,
    text: text,
    unfurl_links: true
  };

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

  UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', options);
}

function getPermalink(token, channelId, ts) {
  const url = `https://slack.com/api/chat.getPermalink?channel=${channelId}&message_ts=${ts}`;
  const res = callSlackApi(token, url);
  return res && res.ok ? res.permalink : '(リンク取得失敗)';
}

function hasResolvedReaction(msg) {
  if (!msg.reactions) return false;
  return msg.reactions.some(r => r.name === CONFIG.RESOLVED_REACTION_NAME);
}

function callSlackApi(token, url) {
  const options = {
    method: 'get',
    headers: { Authorization: `Bearer ${token}` },
    muteHttpExceptions: true
  };
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  if (!json.ok) {
    console.error(`Slack API Error: ${json.error}`, url);
    return {};
  }
  return json;
}

① Slack Botトークンを取得する

Slack APIページでBotを作ってトークンを取得します。

  1. https://api.slack.com/apps にアクセス

  2. 「Create New App」から新しいアプリを作成

  3. 以下のOAuthスコープを付与(Bot Token Scopes):

    • chat:write
    • channels:history
    • channels:read
    • reactions:read
    • users:read
  4. ワークスペースにインストールして、Bot Token(xoxb-...)を取得


② GASプロジェクトを作る

  1. https://script.google.com にアクセスして「新しいプロジェクト」を作成
  2. エディタにコードを貼り付け
  3. メニュー → ファイル → プロジェクトのプロパティ → スクリプトのプロパティ を開く
  4. キー:SLACK_TOKEN、値:取得したトークン を登録

③ 設定項目を編集する

const CONFIG = {
  TARGET_CHANNEL_ID: '監視したいチャンネルID',
  ALERT_CHANNEL_ID: '通知を飛ばすチャンネルID',
  ADMIN_USER_IDS: ['UXXXXXXX1', 'UXXXXXXX2'], // 情シスメンバーのユーザーID
  THRESHOLD_MINUTES: 60,  // 何分放置されたら通知するか
  COOLDOWN_HOURS: 4,      // 同じスレッドに再通知しないクールダウン
  RESOLVED_REACTION_NAME: 'white_check_mark', // ✅ で解決済みとみなす
  MENTION_ADMINS: true    // 通知にメンションをつけるか
};

SlackのチャンネルIDやユーザーIDは、開発者モードで右クリックすると取得可能です。


④ トリガーの設定(定期実行)

GASの画面で
メニュー → トリガー → 新しいトリガーを追加
関数名:checkNeglectedThreads
イベントの種類:時間主導型
→ 「5分おき」「15分おき」などで実行


⑤ ✅ 解決済みの扱い

Slackで対象メッセージに ✅(white_check_mark)リアクションを付けるだけで通知対象外になります。
Botの動作を止めるのも再開するのも手動でコントロール可能。


🔥 どういう時に通知される?

✅が付いてない & 以下すべてに該当するときに通知されます。

  • 最後に返信したのが情シスメンバー以外
  • 最終投稿から 60分以上放置
  • 同じスレッドに 4時間以内に通知してない

🧠 補足:なぜ便利?

  • 「対応漏れ」が起きなくなる
  • ✅で解決管理できるので運用負荷が少ない
  • GASで完結 → 無料で運用コストゼロ
  • 管理者だけに通知できるのでノイズにならない

📌 注意点

  • Slack API呼び出し制限に注意(多いと失敗する)
  • limit で読み込むメッセージ数は調整可能
  • thread_ts の扱いはスレッドの仕様によって異なるので注意

🎉 おわりに

Botって難しそうに見えるけど、ちゃんと構成決めて作れば超シンプル
Slackの質問が放置されがちなチームに導入するとめっちゃ便利です。


✍️ おまけ:コードで工夫してるポイント

  • ✅リアクションを活用して「解決済み」を自動判定
  • 最終発言者が「情シスメンバー以外」かどうかで判断
  • クールダウン機能でスパム通知防止(プロパティで簡単キャッシュ)
  • Slackのパーマリンク付きで、スレッドにすぐ飛べる
0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?