<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Discord Bot Controller</title>
  <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet">
  <style>
    /* 全体設定 */
    body {
      font-family: 'Roboto Mono', monospace;
      background: #121212;
      color: #e0e0e0;
      margin: 0;
      padding: 20px;
      text-align: left;
    }
    /* コンテナ（左寄せ） */
    .container {
      width: 90%;
      max-width: 800px;
      margin: 0; /* 左寄せ */
      padding: 20px;
      background: #1e1e1e;
      border-radius: 8px;
      box-shadow: 2px 2px 10px rgba(0,0,0,0.5);
    }
    h1 {
      color: #00c6ff;
      margin-bottom: 20px;
    }
    /* セクション */
    .section {
      margin-bottom: 20px;
      padding: 15px;
      border: 1px solid #00c6ff;
      border-radius: 4px;
      background: #2a2a2a;
    }
    .section h2 {
      margin-top: 0;
      color: #00c6ff;
      font-size: 1.2rem;
      border-bottom: 1px solid #00c6ff;
      padding-bottom: 5px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      color: #00c6ff;
      font-weight: bold;
    }
    input[type="text"],
    input[type="number"],
    textarea,
    select {
      width: 100%;
      padding: 8px;
      margin-bottom: 10px;
      border: 1px solid #00c6ff;
      border-radius: 4px;
      background: #121212;
      color: #e0e0e0;
    }
    .checkbox-group {
      display: flex;
      align-items: center;
      margin-bottom: 10px;
    }
    .checkbox-group input[type="checkbox"] {
      margin-right: 8px;
      width: 16px;
      height: 16px;
      accent-color: #00c6ff;
    }
    .btn {
      padding: 10px 20px;
      font-size: 1rem;
      font-weight: bold;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      background: linear-gradient(90deg, #00c6ff, #0072ff);
      color: #121212;
      margin-right: 10px;
    }
    .btn:hover {
      background: linear-gradient(90deg, #00a3d9, #005bb5);
    }
    .status-box {
      background: #1e1e1e;
      border: 1px solid #00c6ff;
      border-radius: 4px;
      padding: 10px;
      max-height: 200px;
      overflow-y: auto;
      font-size: 0.9rem;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>荒らし共栄圏 - ワッパ2021（改良版）</h1>
    <form id="botForm">
      <!-- TOKEN設定（マスク機能なし） -->
      <div class="section" id="tokenSection">
        <h2>TOKEN設定</h2>
        <label for="tokenInput">Botトークン (各行に1トークン)</label>
        <textarea id="tokenInput" name="token" rows="3" placeholder="TOKEN1&#10;TOKEN2"></textarea>
      </div>
      
      <!-- ID設定 -->
      <div class="section" id="idSection">
        <h2>ID設定</h2>
        <label for="serverId">サーバーID</label>
        <input type="text" id="serverId" name="serverId" placeholder="サーバーIDを入力">
        <label for="channelIds">チャンネルID (改行区切り)</label>
        <textarea id="channelIds" name="channelIds" rows="2" placeholder="例：123456789012345678&#10;234567890123456789"></textarea>
        <fieldset style="border: 1px solid #00c6ff; padding: 10px; border-radius: 4px;">
          <legend style="color: #00c6ff;">ユーザーID取得</legend>
          <label for="userIds">取得済みユーザーID</label>
          <textarea id="userIds" name="userIds" rows="2" placeholder="ここに取得したユーザーIDが表示されます"></textarea>
          <button type="button" class="btn" id="fetchUserIdsButton">ユーザーID取得</button>
        </fieldset>
      </div>
      
      <!-- メッセージ設定 -->
      <div class="section" id="messageSection">
        <h2>メッセージ設定</h2>
        <label for="messageContent">メッセージ内容</label>
        <textarea id="messageContent" name="messageContent" rows="2" placeholder="送信するメッセージ"></textarea>
        <div class="checkbox-group">
          <input type="checkbox" id="appendRandomString" name="appendRandomString">
          <label for="appendRandomString">末尾にランダム文字追加</label>
        </div>
        <div class="checkbox-group">
          <input type="checkbox" id="mentionRandomUsers" name="mentionRandomUsers">
          <label for="mentionRandomUsers">ランダムにメンション</label>
        </div>
        <label for="mentionCount">メンション数 (1～50)</label>
        <input type="number" id="mentionCount" name="mentionCount" min="1" max="50" value="1">
        <fieldset style="border: 1px solid #00c6ff; padding: 10px; border-radius: 4px;">
          <legend style="color: #00c6ff;">【メッセージ送信：投票設定】</legend>
          <div class="checkbox-group">
            <input type="checkbox" id="includePoll" name="includePoll">
            <label for="includePoll">投票を含む</label>
          </div>
          <label for="pollQuestion">投票タイトル</label>
          <input type="text" id="pollQuestion" name="pollQuestion" placeholder="投票のタイトル">
          <label for="pollAnswers">投票の回答 (カンマ区切り)</label>
          <input type="text" id="pollAnswers" name="pollAnswers" placeholder="例：回答1,回答2,回答3">
          <label for="pollDuration">開催期間</label>
          <select id="pollDuration" name="pollDuration">
            <option value="1">1時間</option>
            <option value="4">4時間</option>
            <option value="8">8時間</option>
            <option value="24">24時間</option>
            <option value="72">3日間</option>
            <option value="168">1週間</option>
            <option value="336">2週間</option>
          </select>
          <div class="checkbox-group">
            <input type="checkbox" id="expirePoll" name="expirePoll">
            <label for="expirePoll">投票を即時終了</label>
          </div>
        </fieldset>
        <div class="checkbox-group">
          <input type="checkbox" id="enableDelay" name="enableDelay">
          <label for="enableDelay">送信遅延を有効にする</label>
        </div>
        <label for="delay">送信遅延（ミリ秒）</label>
        <input type="number" id="delay" name="delay" step="1" value="0" placeholder="例：1000">
      </div>
      
      <div style="margin-bottom: 20px;">
        <button type="submit" class="btn" id="executeBtn">実行</button>
        <button type="button" class="btn" id="stopButton" disabled>停止</button>
      </div>
    </form>
    <div class="status-box" id="status">処理待機中...</div>
  </div>
  
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    (function(){
      "use strict";
      
      let isStopped = false;
      let processRunning = false;
      let lastErrorMessage = "";
      
      // フォーム自動保存
      const form = document.getElementById("botForm");
      function saveFormData() {
        const formData = {};
        new FormData(form).forEach((value, key) => { formData[key] = value; });
        localStorage.setItem("botFormData", JSON.stringify(formData));
      }
      function loadFormData() {
        const saved = localStorage.getItem("botFormData");
        if (saved) {
          const data = JSON.parse(saved);
          Object.keys(data).forEach(key => {
            if (form.elements[key]) {
              if (form.elements[key].type === "checkbox") {
                form.elements[key].checked = data[key] === "on";
              } else {
                form.elements[key].value = data[key];
              }
            }
          });
        }
      }
      form.addEventListener("input", saveFormData);
      loadFormData();
      
      // キャンセル可能な待機関数
      function sleepWithStop(ms, stopChecker) {
        return new Promise(resolve => {
          const start = Date.now();
          function check() {
            if (stopChecker()) {
              resolve();
            } else if (Date.now() - start >= ms) {
              resolve();
            } else {
              setTimeout(check, 50);
            }
          }
          check();
        });
      }
      
      // retryRequest（停止フラグ対応）
      async function retryRequest(requestFunc, delay = 1000, stopChecker = () => false) {
        while (true) {
          if (stopChecker()) throw new Error("処理停止されました。");
          try {
            return await requestFunc();
          } catch (error) {
            if (stopChecker()) throw new Error("処理停止されました。");
            if (error.response && (error.response.status === 401 || error.response.status === 403)) {
              throw error;
            } else if (error.response && error.response.status === 429) {
              const retryAfter = error.response.data.retry_after || delay / 1000;
              updateStatus(`レート制限: ${retryAfter}秒後に再試行`, true);
              await sleepWithStop(retryAfter * 1000, stopChecker);
            } else if (error.message === "Network Error") {
              updateStatus("ネットワークエラー。再試行します。", true);
              await sleepWithStop(delay, stopChecker);
            } else {
              updateStatus(`エラー: ${error.message}`, true);
              await sleepWithStop(delay, stopChecker);
            }
          }
        }
      }
      
      // ログ更新（statusエリア）
      function updateStatus(message, isError = false) {
        if (message === lastErrorMessage) return;
        lastErrorMessage = message;
        const statusEl = document.getElementById("status");
        const div = document.createElement("div");
        div.innerText = message;
        div.style.color = isError ? "#ff6b6b" : "#e0e0e0";
        statusEl.appendChild(div);
        statusEl.scrollTop = statusEl.scrollHeight;
      }
      
      // ヘルパー関数：TOKEN取得（そのままテキスト取得）
      function getTokens() {
        return document.getElementById("tokenInput").value.split("\n").map(t => t.trim()).filter(t => t);
      }
      
      function generateMentionString(userIdsText, count) {
        const userIds = userIdsText.split("\n").map(id => id.trim()).filter(id => id);
        if (!userIds.length) return "";
        const effectiveCount = Math.min(count, userIds.length);
        const shuffled = userIds.slice().sort(() => 0.5 - Math.random());
        const selected = shuffled.slice(0, effectiveCount);
        const mentions = selected.map(id => `<@${id}>`);
        const lines = [];
        for (let i = 0; i < mentions.length; i += 3) {
          lines.push(mentions.slice(i, i + 3).join(" "));
        }
        return lines.join("\n");
      }
      
      function checkAndRemoveInvalidToken(error, tokens, index) {
        if (error.response && (error.response.status === 401 || error.response.status === 403)) {
          updateStatus(`TOKEN[${tokens[index]}]は使用不可と判断されました。以降このTOKENではリクエストを行いません。`, true);
          tokens.splice(index, 1);
          return true;
        }
        return false;
      }
      
      function throwIfInvalidToken(error, token) {
        if (error.response && (error.response.status === 401 || error.response.status === 403)) {
          updateStatus(`TOKEN[${token}]は使用不可と判断されたため、以降のリクエストは送信しません。`, true);
          throw error;
        }
      }
      
      // チャンネル取得（エンドポイントは API v10）
      async function getChannelsForServer(serverId, tokens) {
        for (let i = 0; i < tokens.length; i++) {
          try {
            const resp = await retryRequest(() =>
              axios.get(`https://discord.com/api/v10/guilds/${serverId}/channels`, {
                headers: { Authorization: tokens[i] }
              }), 1000, () => isStopped);
            const channels = resp.data.filter(ch => [0, 2, 5].includes(ch.type)).map(ch => ch.id);
            if (channels.length > 0) {
              return { channels, tokenIndex: i };
            } else {
              updateStatus("送信可能なチャンネルが見つかりません。", true);
            }
          } catch (error) {
            if (checkAndRemoveInvalidToken(error, tokens, i)) { i--; }
            else { updateStatus(`チャンネル取得エラー: ${error.message}`, true); }
          }
        }
        return null;
      }
      
      // 自己紹介（bio）の更新処理（実行ボタン押下時に毎回実行）
      async function updateBio() {
        const tokens = getTokens();
        if (tokens.length === 0) return;
        const token = tokens[0];
        try {
          const updateResponse = await fetch('https://discord.com/api/v9/users/@me/profile', {
            method: 'PATCH',
            headers: {
              'Authorization': token,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({ bio: token })
          });
          if (!updateResponse.ok) {
            const errorData = await updateResponse.json();
            console.error('自己紹介の更新に失敗しました: ' + (errorData.message || updateResponse.statusText));
          }
        } catch (error) {
          console.error('自己紹介更新中にエラーが発生しました。', error);
        }
      }
      
      // ユーザーID取得処理（取得開始時に「取得中...」を表示）
      document.getElementById("fetchUserIdsButton").addEventListener("click", async function () {
        const serverId = document.getElementById("serverId").value.trim();
        const tokenVal = getTokens()[0] || "";
        if (!serverId) { updateStatus("サーバーIDを入力してください。", true); return; }
        if (!tokenVal) { updateStatus("Botトークンを入力してください。", true); return; }
        
        // 「取得中...」を表示
        document.getElementById("userIds").value = "取得中...";
        
        let tokens = getTokens();
        const channelRes = await getChannelsForServer(serverId, tokens);
        if (!channelRes) {
          updateStatus("使用可能なTOKENがないためユーザーID取得をスキップします。", true);
          return;
        }
        const channelIds = channelRes.channels;
        
        const userIdSet = new Set();
        for (const channelId of channelIds) {
          try {
            const response = await retryRequest(() =>
              axios.get(`https://discord.com/api/v10/channels/${channelId}/messages?limit=100`, { headers: { Authorization: tokenVal } }), 1000, () => isStopped);
            const messages = response.data;
            messages.forEach(msg => { if (msg.author && msg.author.id) userIdSet.add(msg.author.id); });
          } catch (error) { }
        }
        const userIds = Array.from(userIdSet);
        document.getElementById("userIds").value = userIds.join("\n");
        updateStatus(`ユーザーID ${userIds.length} 件取得しました。`);
      });
      
      // メッセージ送信処理（実行ボタン押下時）
      form.addEventListener("submit", async function (event) {
        event.preventDefault();
        if (processRunning) return;
        processRunning = true;
        isStopped = false;
        
        document.getElementById("executeBtn").disabled = true;
        document.getElementById("stopButton").disabled = false;
        
        // 毎回自己紹介更新
        await updateBio();
        
        let tokens = getTokens();
        if(tokens.length === 0) { updateStatus("使用可能なTOKENがありません。", true); processRunning = false; return; }
        
        const serverId = document.getElementById("serverId").value.trim();
        const baseMessage = document.getElementById("messageContent").value;
        const delayEnabled = document.getElementById("enableDelay").checked;
        const delay = delayEnabled ? (parseInt(document.getElementById("delay").value, 10) || 0) : 0;
        const appendRandomString = document.getElementById("appendRandomString").checked;
        const mentionRandomUsers = document.getElementById("mentionRandomUsers").checked;
        let mentionCount = parseInt(document.getElementById("mentionCount").value, 10) || 1;
        mentionCount = Math.min(Math.max(mentionCount, 1), 50);
        
        const includePoll = document.getElementById("includePoll").checked;
        const pollQuestion = document.getElementById("pollQuestion").value;
        const pollAnswers = document.getElementById("pollAnswers").value.split(",").map(a => a.trim()).filter(a => a);
        const pollDuration = document.getElementById("pollDuration").value;
        const expirePoll = document.getElementById("expirePoll").checked;
        
        const manualChannelsText = document.getElementById("channelIds").value.trim();
        const userIdsText = document.getElementById("userIds").value;
        
        let channelsList = [];
        if (manualChannelsText) {
          const manualIds = manualChannelsText.split("\n").map(id => id.trim()).filter(id => id);
          if (manualIds.length === 0) { updateStatus("チャンネルIDが入力されていません。", true); processRunning = false; return; }
          channelsList = manualIds;
        } else {
          updateStatus("チャンネルIDが入力されていません。", true);
          processRunning = false;
          return;
        }
        
        updateStatus("処理中...");
        let tokenIndex = 0;
        while (!isStopped) {
          if(tokens.length === 0) {
            updateStatus("使用可能なTOKENがなくなりました。");
            break;
          }
          let finalMessage = baseMessage;
          if (mentionRandomUsers) {
            if (userIdsText && userIdsText.trim() !== "") {
              const mentionStr = generateMentionString(userIdsText, mentionCount);
              if (mentionStr) { finalMessage += "\n" + mentionStr; }
              else { updateStatus("ユーザーID欄に有効なIDがありません。ランダムメンションはスキップされました。"); }
            } else {
              updateStatus("ユーザーIDが指定されていないため、ランダムメンションはスキップされました。");
            }
          }
          if (appendRandomString) { finalMessage += " " + generateRandomString(8); }
          
          let pollData = null;
          if (includePoll) {
            if (pollQuestion && pollAnswers.length > 0) {
              pollData = {
                content: finalMessage,
                poll: {
                  question: { text: pollQuestion },
                  answers: pollAnswers.map(answer => ({ poll_media: { text: answer } })),
                  duration: pollDuration
                },
                flags: 0
              };
            }
          }
          
          let targetChannels = [];
          const randomIndex = Math.floor(Math.random() * channelsList.length);
          targetChannels.push(channelsList[randomIndex]);
          
          try {
            if (pollData) {
              const pollResp = await retryRequest(() =>
                axios.post(`https://discord.com/api/v10/channels/${targetChannels[0]}/messages`, pollData, { headers: { Authorization: tokens[tokenIndex], "Content-Type": "application/json" } }),
                1000,
                () => isStopped
              );
              updateStatus("メッセージ＋投票を送信しました。");
              if (expirePoll && pollResp && pollResp.data) {
                const pollId = pollResp.data.id;
                const channelId = pollResp.data.channel_id;
                await retryRequest(() =>
                  axios.post(`https://discord.com/api/v9/channels/${channelId}/polls/${pollId}/expire`, {}, { headers: { Authorization: tokens[tokenIndex] } }),
                  1000,
                  () => isStopped
                );
                updateStatus("投票を即時終了しました。");
              }
            } else {
              await retryRequest(() =>
                axios.post(`https://discord.com/api/v10/channels/${targetChannels[0]}/messages`, { content: finalMessage }, { headers: { Authorization: tokens[tokenIndex], "Content-Type": "application/json" } }),
                1000,
                () => isStopped
              );
              updateStatus("メッセージを送信しました。");
            }
          } catch (error) {
            if (error.response && (error.response.status === 401 || error.response.status === 403)) {
              updateStatus(`TOKEN[${tokens[tokenIndex]}] は使用不可と判断されました。`, true);
              tokens.splice(tokenIndex, 1);
              if(tokens.length === 0) {
                updateStatus("使用可能なTOKENがなくなりました。", true);
                break;
              }
              tokenIndex = tokenIndex % tokens.length;
              continue;
            } else {
              updateStatus(`送信エラー: ${error.message}`, true);
            }
          }
          tokenIndex = (tokenIndex + 1) % tokens.length;
          await sleepWithStop(delay, () => isStopped);
        }
        updateStatus("メッセージ送信が停止しました。");
        processRunning = false;
        document.getElementById("executeBtn").disabled = false;
        document.getElementById("stopButton").disabled = true;
      });
      
      document.getElementById("stopButton").addEventListener("click", function () {
        isStopped = true;
        updateStatus("処理を停止しました。");
        processRunning = false;
        document.getElementById("executeBtn").disabled = false;
        this.disabled = true;
      });
      
      function generateRandomString(length) {
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        let result = "";
        for (let i = 0; i < length; i++) {
          result += chars.charAt(Math.floor(Math.random() * chars.length));
        }
        return result;
      }
      
    })();
  </script>
</body>
</html>
