💪

Chatworkbot byめろん

に公開

初めて書いております!!
今回は僕が実際に使っているChatworkの万能コードの、

荒らし対処 TOALL、TO、文字制限、などの制限、
(管理者権限では作動しません、メンバー権限のみです。ご了承ください。)
RMR(RoomMessageRanking) 部屋のメンバーごとの発言数をそれぞれランキング形式にしたもの、
時報、
などなど!様々なものをまとめて一つにした夢のbot(笑)です!

コピペしてご使用可能です
トリガーなどという設定するものもございますので焦らず最後までご覧ください。

bot専用のアカウントを作りましょう(推薦)

ごちゃごちゃになってしまいがちです!なるべく作っておきましょう!

使用するサイト

https://developers.google.com/apps-script?hl=ja

少し下の方の”スクリプトを開始する”というところを押してください。
そして、

この画像の赤で囲ってある、”+新しいプロジェクト”というところを押して↓に進みましょう

いちばん大事なコード

// ==========================
// Chatwork Bot (GAS版)
// ==========================

// 🔧 Chatwork API
const CHATWORK_API_BASE = 'https://api.chatwork.com/v2';

// ==========================
// 🚪 Webhook入口
// ==========================
function doPost(e) {
  try {
    const props = PropertiesService.getScriptProperties();
    const API_KEY = props.getProperty("CHATWORK_API_KEY");
    const MY_ACCOUNT_ID = props.getProperty("MY_ACCOUNT_ID");

    const payload = JSON.parse(e.postData.contents);
    const event = payload.webhook_event;
    const message = event.body;
    const roomId = event.room_id;
    const messageId = event.message_id;
    const fromAccountId = event.account_id;

    // 🚫 Bot自身の発言は無視
    if (fromAccountId == MY_ACCOUNT_ID) {
      return ContentService.createTextOutput("OK");
    }

    // 🛑 重複処理防止
    const processed = JSON.parse(
      PropertiesService.getUserProperties().getProperty("processedMessages") || "{}"
    );
    if (processed[messageId]) return ContentService.createTextOutput("OK");

    // ==============================
    // ① BOT宛てリプライ禁止
    // ==============================
    const replyMatch = message.match(/\[rp aid=(\d+) to=[\d\-]+\]/);
    if (replyMatch && replyMatch[1] == MY_ACCOUNT_ID) {
      sendReply(API_KEY, roomId, messageId, fromAccountId, "リプライしないでください。");
      markProcessed(processed, messageId);
      return ContentService.createTextOutput("OK");
    }

    // ==============================
    // ② toall 検知
    // ==============================
    if (/\[toall\]/gi.test(message)) {
      const members = getChatworkMembers(API_KEY, roomId);
      const me = members.find((m) => m.account_id === fromAccountId);
      if (me && me.role !== "admin") {
        moveToReadonly(API_KEY, roomId, fromAccountId);
      } else {
        sendCW(API_KEY, roomId, "管理者がtoallを使用しました。見逃してあげてください");
      }
      markProcessed(processed, messageId);
      return ContentService.createTextOutput("OK");
    }

    // ==============================
    // ③ 新規メンバー歓迎
    // ==============================
    if (message.includes("[dtext:chatroom_member_is]") && message.includes("[dtext:chatroom_added]")) {
      const regex = /\[piconname:(\d+)\]/g;
      const members = getChatworkMembers(API_KEY, roomId);
      let match;
      while ((match = regex.exec(message)) !== null) {
        const newMemberId = match[1];
        if (newMemberId != MY_ACCOUNT_ID) {
          const memberInfo = members.find(m => m.account_id == newMemberId);
          const memberName = memberInfo ? memberInfo.name : "さん";
          const welcome = `[To:${newMemberId}] ${memberName} さん\nようこそ!概要を読んでおいてください!`;
          sendCW(API_KEY, roomId, welcome);
        }
      }
    }

    // ==============================
    // ④ ランキング (/rmr)
    // ==============================
    updateRanking(roomId, fromAccountId);
    if (message.includes("/rmr")) {
      const ranking = getRanking(roomId);
      const reply = formatRanking(ranking, fromAccountId, roomId, messageId);
      sendCW(API_KEY, roomId, reply);
    }

    // ==============================
    // ⑤ スパム検知
    // ==============================
    if (detectSpam(message)) {
      moveToReadonly(API_KEY, roomId, fromAccountId);
    } else {
      // 追加の通常応答
      const reply = getReplyMessage(message, fromAccountId, roomId, messageId);
      if (reply) {
        sendReply(API_KEY, roomId, messageId, fromAccountId, reply);
      }
    }

    // ✅ 処理済みフラグ保存
    markProcessed(processed, messageId);
    return ContentService.createTextOutput("OK");

  } catch (err) {
    Logger.log("Error: " + err);
    return ContentService.createTextOutput("Error");
  }
}

// ==========================
// 📊 ランキング関連
// ==========================
function updateRanking(roomId, accountId) {
  const key = `ranking_${roomId}_${getToday()}`;
  const props = PropertiesService.getScriptProperties();
  const data = JSON.parse(props.getProperty(key) || '{}');
  data[accountId] = (data[accountId] || 0) + 1;
  props.setProperty(key, JSON.stringify(data));
}

function getRanking(roomId) {
  const key = `ranking_${roomId}_${getToday()}`;
  const props = PropertiesService.getScriptProperties();
  return JSON.parse(props.getProperty(key) || '{}');
}

function formatRanking(ranking, accountId, roomId, messageId) {
  const entries = Object.entries(ranking);
  if (entries.length === 0) {
    return `[rp aid=${accountId} to=${roomId}-${messageId}]\n本日のランキングはまだありません。`;
  }

  entries.sort((a, b) => b[1] - a[1]);
  let total = 0;
  let result = `[rp aid=${accountId} to=${roomId}-${messageId}][pname:${accountId}] さん\n`;
  result += '[info][title]本日のコメント数ランキング[/title]\n';

  entries.forEach(([id, count], idx) => {
    result += `[download:1681682877]${idx + 1}位[/download] [piconname:${id}] さん - ${count} コメント[hr]`;
    total += count;
  });

  result += `[hr][hr]合計コメント数: ${total} 件\n`;
  result += '[/info]';
  return result;
}

function resetDailyData() {
  const today = getToday();
  const props = PropertiesService.getScriptProperties();
  const keys = props.getKeys();
  keys.forEach(k => { if (!k.endsWith(today)) props.deleteProperty(k); });
}


// ==========================
// 🛡 スパム検知
// ==========================
function detectSpam(text) {
  const EMOJI_LIST = [
    ":", "(", ":D", "8-)", ":o", ";)", ";(", "(sweat)", ":|", ":*", ":p", 
    "(blush)", ":^)", "|-)", "(inlove)", "]:)", "(talk)", "(yawn)", 
    "(puke)", "(emo)", "8-|", ":#)", "(nod)", "(shake)", "(^^;)", "(whew)", 
    "(clap)", "(bow)", "(roger)", "(flex)", "(dance)", "(:/)", "(gogo)", 
    "(think)", "(please)", "(quick)", "(anger)", "(devil)", "(lightbulb)", 
    "(*)", "(h)", "(F)", "(cracker)", "(eat)", "(^)", "(coffee)", "(beer)", 
    "(handshake)", "(y)"
  ];
  let emojiCount = 0;
  for (const emoji of EMOJI_LIST) {
    let index = -1;
    while ((index = text.indexOf(emoji, index + 1)) !== -1) {
      emojiCount++;
    }
  }
  const toallCount = (text.match(/\[toall\]/gi) || []).length;
  const toCount = (text.match(/\[To:\d+\]/g) || []).length;
  const qtCount = (text.match(/\[qt\]/g) || []).length;
  const prCount = (text.match(/\[preview/g) || []).length;
  const hrCount = (text.match(/\[hr\]/g) || []).length;
  const rCount = (text.match(/荒らし対処test/g) || []).length;
  const zalgoCount = (text.match(/[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/g) || []).length;
  const MAX_LENGTH = 1000;
  const lengthOver = text.length >= MAX_LENGTH;
  return (emojiCount >= 30 || toallCount >= 1 || toCount >= 5 || rCount >= 1 || qtCount >= 4 || prCount >= 5 || zalgoCount >= 1 || hrCount >= 15 || lengthOver);
}

// ==========================
// 👥 メンバー操作
// ==========================
function moveToReadonly(API_KEY, roomId, targetAccountId) {
  const url = `${CHATWORK_API_BASE}/rooms/${roomId}/members`;
  const members = getChatworkMembers(API_KEY, roomId);
  if (!members) return;

  const target = members.find(m => m.account_id === targetAccountId);
  if (!target || target.role === 'admin') return;

  const adminIds = members.filter(m => m.role === 'admin').map(m => m.account_id);
  const memberIds = members.filter(m => m.role === 'member' && m.account_id !== targetAccountId).map(m => m.account_id);
  const readonlyIds = members.filter(m => m.role === 'readonly').map(m => m.account_id);
  readonlyIds.push(targetAccountId);

  const payload = {
    'members_admin_ids': adminIds.join(','),
    'members_member_ids': memberIds.join(','),
    'members_readonly_ids': readonlyIds.join(',')
  };

  UrlFetchApp.fetch(url, {
    method: 'put',
    payload: payload,
    headers: { 'X-ChatWorkToken': API_KEY },
    muteHttpExceptions: true
  });

  sendCW(API_KEY, roomId, `[info][title]不正利用記録[/title][To:10429722] 優一\n[piconname:${targetAccountId}] さんに対して、不正利用フィルターが発動しました。[/info]`);
}

function getChatworkMembers(API_KEY, roomId) {
  const url = `${CHATWORK_API_BASE}/rooms/${roomId}/members`;
  const res = UrlFetchApp.fetch(url, { method: 'get', headers: { 'X-ChatWorkToken': API_KEY } });
  return res.getResponseCode() === 200 ? JSON.parse(res.getContentText()) : null;
}

// ==========================
// 💬 メッセージ送受信
// ==========================
function sendReply(API_KEY, roomId, messageId, accountId, replyMessage) {
  const body = `[rp aid=${accountId} to=${roomId}-${messageId}][pname:${accountId}] さん\n${replyMessage}`;
  sendCW(API_KEY, roomId, body);
}

function sendCW(API_KEY, roomId, message) {
  UrlFetchApp.fetch(`${CHATWORK_API_BASE}/rooms/${roomId}/messages`, {
    method: 'POST',
    headers: { 'X-ChatWorkToken': API_KEY },
    payload: { body: message }
  });
}

// ==========================
// 🕒 時報
// ==========================
function sendHourlyReport() {
  const props = PropertiesService.getScriptProperties();
  const API_KEY = props.getProperty("CHATWORK_API_KEY");
  const ROOM_IDS = ["408052452", "397335695","403451710","409045701","409301619"];

  const now = new Date();
  const hour = now.getHours();
  const days = ["日", "月", "火", "水", "木", "金", "土"];
  const day = days[now.getDay()];
  const dateStr = Utilities.formatDate(now, "Asia/Tokyo", "yyyy/MM/dd");

  const message = `[info][title]時報[/title]現在 ${dateStr} (${day}) ${hour} 時です。[/info]`;
  ROOM_IDS.forEach(roomId => sendCW(API_KEY, roomId, message));
}

// ==========================
// 🛠 補助関数
// ==========================
function getReplyMessage(message) {
  const replies = {
    "Test": "テストメッセージだ!動いてるよ!",
    "test": "testメッセージを取得したよ!確認ありがとう!"
  };
  return replies[message] || null;
}

function markProcessed(processed, messageId) {
  processed[messageId] = true;
  PropertiesService.getUserProperties().setProperty("processedMessages", JSON.stringify(processed));
}

function getToday() {
  return Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyyMMdd");
}

必要なプロパティ

プロパティの左側
CHATWORK_API_KEY https://www.chatwork.com/service/packages/chatwork/subpackages/api/token.php こちらから取得可能です。
MY_ACCOUNT_ID このリンクは、アカウントIDの確認の仕方です。

必要なトリガー

関数 時間ベース 時刻、時間の選択
sendHourlyReport 日付ベースのタイマー 午前0時〜1時(デフォルト)
resetDailyData 日付ベースのタイマー 午前0時〜1時(デフォルト)
updateRanking 分ベースのタイマー 1分おき(デフォルト)

チャットワーク側の設定

1.このリンクから入る

2.新規作成を選択、

その後
・ルームイベントを選択
・メッセージ作成にチェックを入れる
・ルームIDにbotを使用するルームのIDを入れる

名前(場所) 入れるもの
Webhook名 自分がわかれば何でも良し!
Webhook URL 先ほどコピーしたWebアプリのURL
ROOM ID 例:以下の数字の箇所、「””」で囲われている部分です。 https://www.chatwork.com/#!rid"123456789"

確認など

(bot側の視点になってしまいました申し訳ございません)
ばっちりだね!確認完了!お疲れ様でした〜!

なにかご質問や不明点がございましたら言ってもらえると幸いです:)

Discussion