您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Discordの複垢管理ツール(ユーザー情報をトークンごとにキャッシュして冗長な再取得を防止)
// ==UserScript== // @name Discord複数アカウント管理(キャッシュ対応版) // @namespace http://tampermonkey.net/ // @version 1.2 // @description Discordの複垢管理ツール(ユーザー情報をトークンごとにキャッシュして冗長な再取得を防止) // @author Freeze // @match https://discord.com/* // @grant GM_setValue // @grant GM_getValue // @license You can modify as long as you credit me // ==/UserScript== (function() { 'use strict'; /***—— 定数・初期値 ——————————————————————————————————————————***/ const maxGroups = ['A', 'B', 'C']; let currentGroup = GM_getValue('currentGroup', 'A'); let isBoxVisible = GM_getValue('isBoxVisible', false); /***—— ユーザー情報キャッシュ管理関数 ——————————————————————————***/ // キャッシュは { "<token>": "displayString", ... } の形式で保持する function loadUserInfoCache() { const raw = GM_getValue('userInfoCache', '{}'); try { return JSON.parse(raw); } catch { return {}; } } function saveUserInfoCache(cacheObj) { GM_setValue('userInfoCache', JSON.stringify(cacheObj)); } function getCachedUserInfo(token) { const cache = loadUserInfoCache(); return cache[token] || null; } function setCachedUserInfo(token, displayString) { const cache = loadUserInfoCache(); cache[token] = displayString; saveUserInfoCache(cache); } /***—— Discord APIからユーザー情報を取得して<span>に表示し、キャッシュも更新 ——————————***/ function fetchAndDisplayUserInfo(token, targetSpan) { fetch('https://discord.com/api/v9/users/@me', { headers: { 'Authorization': token } }) .then(res => { if (res.ok) return res.json(); throw new Error('Invalid token'); }) .then(data => { // discriminator廃止後 → username のみ。global_nameがあれば併記 const usernameOnly = data.username; const globalName = data.global_name || ''; const displayText = globalName ? `${usernameOnly}(${globalName})` : usernameOnly; targetSpan.textContent = displayText; // キャッシュに保存 setCachedUserInfo(token, displayText); }) .catch(() => { targetSpan.textContent = '無効なトークン'; }); } /***—— トグルボタン ——————————————————————————————————————————***/ const toggleBtn = document.createElement('button'); toggleBtn.textContent = 'アカウント管理'; Object.assign(toggleBtn.style, { position: 'fixed', bottom: '160px', right: '20px', padding: '8px 12px', backgroundColor: '#5865f2', color: '#ffffff', border: 'none', borderRadius: '6px', fontSize: '14px', cursor: 'pointer', zIndex: '1001', boxShadow: '0 2px 6px rgba(0,0,0,0.3)', transition: 'background-color 0.2s', }); toggleBtn.addEventListener('mouseenter', () => toggleBtn.style.backgroundColor = '#4752c4'); toggleBtn.addEventListener('mouseleave', () => toggleBtn.style.backgroundColor = '#5865f2'); document.body.appendChild(toggleBtn); toggleBtn.addEventListener('click', () => { isBoxVisible = !isBoxVisible; mainContainer.style.display = isBoxVisible ? 'block' : 'none'; GM_setValue('isBoxVisible', isBoxVisible); }); /***—— メインコンテナ ——————————————————————————————————————————***/ const container = document.createElement('div'); container.innerHTML = ` <div id="mainContainer" style=" position: fixed; bottom: 200px; right: 20px; width: 380px; height: 700px; background-color: #2f3136; color: #ffffff; border: 1px solid #202225; border-radius: 8px; z-index: 1000; box-shadow: 0 2px 10px rgba(0,0,0,0.5); font-family: 'Segoe UI', sans-serif; "> <!-- タイトルバー --> <div id="titleBar" style=" display: flex; justify-content: space-between; align-items: center; background-color: #202225; padding: 10px 14px; border-top-left-radius: 8px; border-top-right-radius: 8px; user-select: none; "> <span style="font-size: 16px; font-weight: 600;">Discordアカウント管理</span> <button id="closeButton" style=" background: none; border: none; color: #b9bbbe; font-size: 18px; cursor: pointer; ">×</button> </div> <!-- コンテンツ本体 --> <div id="content" style=" display: block; padding: 14px; max-height: 632px; /* 700px - タイトルバー(≈38px) */ overflow-y: auto; "> <!-- グループ切り替え --> <div id="groupButtons" style=" display: flex; justify-content: space-between; margin-bottom: 18px; "> ${maxGroups.map(g => ` <button class="groupBtn" data-group="${g}" style=" flex: 1; margin-right: ${g !== 'C' ? '8px' : '0'}; padding: 10px 0; background-color: #2f3136; color: #ffffff; border: 1px solid #36393f; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; "> グループ${g} </button> `).join('')} </div> <!-- TOKEN セクション --> <div id="tokenSection" style=" border: 1px solid #36393f; border-radius: 6px; margin-bottom: 18px; background-color: #2f3136; "> <div style=" background-color: #5865f2; padding: 8px 12px; border-top-left-radius: 6px; border-top-right-radius: 6px; "> <span style="color: #ffffff; font-size: 14px; font-weight: 500;">TOKEN</span> </div> <div id="tokenList" style="padding: 12px;"> <!-- トークン行はここに JavaScript で動的に追加 --> </div> <div style="padding: 0 12px 12px 12px;"> <button id="addTokenBtn" style=" width: 100%; padding: 10px; background-color: #43b581; color: #ffffff; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background-color 0.2s; ">+ トークン追加</button> </div> </div> <!-- リダイレクト・チャンネル URL 入力 --> <div style="margin-bottom: 14px; display: flex; align-items: center;"> <input type="text" id="urlInput" placeholder="リダイレクト用招待URL" style=" flex: 1; padding: 8px; background-color: #202225; color: #ffffff; border: 1px solid #5865f2; border-radius: 4px; font-size: 13px; "> <span style="margin-left: 6px; color: #b9bbbe; font-size: 12px;">(任意)</span> </div> <div style="margin-bottom: 18px; display: flex; align-items: center;"> <input type="text" id="channelUrlInput" placeholder="チャンネル/メッセージURL" style=" flex: 1; padding: 8px; background-color: #202225; color: #ffffff; border: 1px solid #5865f2; border-radius: 4px; font-size: 13px; "> <span style="margin-left: 6px; color: #b9bbbe; font-size: 12px;">(任意)</span> </div> <!-- 新しいタブで開く チェックボックス --> <div style="display: flex; align-items: center; margin-bottom: 18px;"> <input type="checkbox" id="newTabCheckbox" style="width: 18px; height: 18px; margin-right: 8px;"> <label for="newTabCheckbox" style="color: #ffffff; font-size: 14px; cursor: pointer;">新しいタブで開く</label> </div> <!-- 警告メッセージ --> <div style="margin-bottom: 8px;"> <span style="color: #faa81a; font-size: 13px;">⚠️ ログアウトするとトークンがリセットされます</span> </div> </div> </div> `; document.body.appendChild(container); const mainContainer = document.getElementById('mainContainer'); const content = document.getElementById('content'); const closeButton = document.getElementById('closeButton'); const addTokenBtn = document.getElementById('addTokenBtn'); const tokenList = document.getElementById('tokenList'); const urlInput = document.getElementById('urlInput'); const channelUrlInput = document.getElementById('channelUrlInput'); const newTabCheckbox = document.getElementById('newTabCheckbox'); const groupButtons = document.querySelectorAll('.groupBtn'); /***—— 初期表示設定 ——————————————————————————————————————————***/ function renderContainerState() { content.style.display = 'block'; } renderContainerState(); mainContainer.style.display = isBoxVisible ? 'block' : 'none'; function updateGroupButtonStyles() { groupButtons.forEach(btn => { if (btn.dataset.group === currentGroup) { btn.style.backgroundColor = '#5865f2'; btn.style.color = '#ffffff'; btn.style.borderColor = '#5865f2'; } else { btn.style.backgroundColor = '#2f3136'; btn.style.color = '#b9bbbe'; btn.style.borderColor = '#36393f'; btn.addEventListener('mouseenter', () => { btn.style.backgroundColor = '#3a3c43'; }); btn.addEventListener('mouseleave', () => { if (btn.dataset.group !== currentGroup) btn.style.backgroundColor = '#2f3136'; }); } }); } updateGroupButtonStyles(); // グループごとの保存データを初期ロード urlInput.value = GM_getValue(`${currentGroup}_urlInput`, ''); channelUrlInput.value = GM_getValue(`${currentGroup}_channelUrlInput`, ''); newTabCheckbox.checked = GM_getValue('newTabCheckbox', false); closeButton.addEventListener('click', () => { isBoxVisible = false; mainContainer.style.display = 'none'; GM_setValue('isBoxVisible', isBoxVisible); }); /***—— グループ切り替えロジック ——————————————————————————***/ groupButtons.forEach(btn => { btn.addEventListener('click', () => { saveTokenRowsToStorage(); currentGroup = btn.dataset.group; GM_setValue('currentGroup', currentGroup); loadTokensFromStorage(); // グループごとの URL/ChannelURL を即時反映 urlInput.value = GM_getValue(`${currentGroup}_urlInput`, ''); channelUrlInput.value = GM_getValue(`${currentGroup}_channelUrlInput`, ''); newTabCheckbox.checked = GM_getValue('newTabCheckbox', false); updateGroupButtonStyles(); }); }); /***—— トークン行 を動的に管理 —————————————————————————***/ function createTokenRow(savedToken = '') { // 親コンテナ:上下2段で配置 const rowDiv = document.createElement('div'); rowDiv.className = 'tokenRow'; Object.assign(rowDiv.style, { display: 'flex', flexDirection: 'column', backgroundColor: '#2f3136', padding: '8px', borderRadius: '4px', marginBottom: '10px', transition: 'background-color 0.2s', }); rowDiv.addEventListener('mouseenter', () => rowDiv.style.backgroundColor = '#3a3c43'); rowDiv.addEventListener('mouseleave', () => rowDiv.style.backgroundColor = '#2f3136'); // —— 上段:トークン入力 + ボタン群 —— const topRow = document.createElement('div'); topRow.style.display = 'flex'; topRow.style.alignItems = 'center'; // トークン入力フィールド const input = document.createElement('input'); input.type = 'text'; input.value = savedToken; input.placeholder = 'MTM1NjI0NzYwNzQ1MzEyMzQ1…'; Object.assign(input.style, { flex: '1', padding: '6px 8px', marginRight: '8px', backgroundColor: '#202225', color: '#32CD32', border: '1px solid #32CD32', borderRadius: '4px', fontSize: '13px' }); input.addEventListener('input', () => { // 入力が変わったらキャッシュを確認し、必要なら保存処理を呼ぶ saveTokenRowsToStorage(); }); // 「ログイン」ボタン const loginBtn = document.createElement('button'); loginBtn.textContent = 'ログイン'; Object.assign(loginBtn.style, { padding: '6px 10px', marginRight: '6px', backgroundColor: '#5865f2', color: '#ffffff', border: 'none', borderRadius: '4px', fontSize: '13px', cursor: 'pointer', transition: 'background-color 0.2s' }); loginBtn.addEventListener('mouseenter', () => loginBtn.style.backgroundColor = '#4752c4'); loginBtn.addEventListener('mouseleave', () => loginBtn.style.backgroundColor = '#5865f2'); // 「削除」ボタン const delBtn = document.createElement('button'); delBtn.textContent = '削除'; Object.assign(delBtn.style, { padding: '6px 10px', backgroundColor: '#ed4245', color: '#ffffff', border: 'none', borderRadius: '4px', fontSize: '13px', cursor: 'pointer', transition: 'background-color 0.2s' }); delBtn.addEventListener('mouseenter', () => delBtn.style.backgroundColor = '#c23b3b'); delBtn.addEventListener('mouseleave', () => delBtn.style.backgroundColor = '#ed4245'); // ログインボタン押下時の動作 loginBtn.addEventListener('click', () => { const token = input.value.trim(); if (!token) { alert('有効なトークンを入力してください!'); return; } doLoginWithToken(token); setActiveTokenRow(rowDiv); GM_setValue(`${currentGroup}_lastToken`, token); saveTokenRowsToStorage(); }); // 削除ボタン押下時 delBtn.addEventListener('click', () => { tokenList.removeChild(rowDiv); saveTokenRowsToStorage(); }); topRow.appendChild(input); topRow.appendChild(loginBtn); topRow.appendChild(delBtn); // —— 下段:ユーザー情報表示用の<span> —— const bottomRow = document.createElement('div'); bottomRow.style.display = 'flex'; bottomRow.style.alignItems = 'center'; bottomRow.style.marginTop = '6px'; bottomRow.style.marginLeft = '4px'; // 入力欄と揃える余白 const userInfoSpan = document.createElement('span'); Object.assign(userInfoSpan.style, { fontSize: '12px', color: '#b9bbbe', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '260px' }); // フォーカスアウト時にユーザー情報を取得 or キャッシュを反映 input.addEventListener('blur', () => { const token = input.value.trim(); if (token) { const cached = getCachedUserInfo(token); if (cached) { // キャッシュがあればそれを表示 userInfoSpan.textContent = cached; } else { // キャッシュがなければ API で取得してキャッシュ fetchAndDisplayUserInfo(token, userInfoSpan); } } else { userInfoSpan.textContent = ''; } }); // 既存トークンがあれば、キャッシュをチェックして初期表示 if (savedToken) { const cached = getCachedUserInfo(savedToken); if (cached) { userInfoSpan.textContent = cached; } else { fetchAndDisplayUserInfo(savedToken, userInfoSpan); } } bottomRow.appendChild(userInfoSpan); // 行を組み立てる rowDiv.appendChild(topRow); rowDiv.appendChild(bottomRow); return rowDiv; } function saveTokenRowsToStorage() { const rows = tokenList.querySelectorAll('.tokenRow'); const tokens = []; rows.forEach(r => { const val = r.querySelector('input').value; tokens.push(val); }); GM_setValue(`${currentGroup}_tokens`, tokens.join('\n')); } function loadTokensFromStorage() { tokenList.innerHTML = ''; const raw = GM_getValue(`${currentGroup}_tokens`, ''); if (raw !== '') { raw.split('\n').forEach(token => { const row = createTokenRow(token); tokenList.appendChild(row); }); } // 最後に使ったトークンをハイライト const lastToken = GM_getValue(`${currentGroup}_lastToken`, ''); if (lastToken) { tokenList.querySelectorAll('.tokenRow').forEach(r => { const txt = r.querySelector('input').value.trim(); if (txt === lastToken) { setActiveTokenRow(r); } }); } // 保存が空なら空行を1つ生成 if (!tokenList.querySelector('.tokenRow')) { const blank = createTokenRow(''); tokenList.appendChild(blank); } } function setActiveTokenRow(rowDiv) { tokenList.querySelectorAll('.tokenRow button').forEach(btn => { if (btn.textContent === 'ログイン') btn.style.backgroundColor = '#5865f2'; }); const loginBtn = rowDiv.querySelector('button'); loginBtn.style.backgroundColor = '#2f8bfd'; } loadTokensFromStorage(); addTokenBtn.addEventListener('click', () => { const newRow = createTokenRow(''); tokenList.appendChild(newRow); saveTokenRowsToStorage(); newRow.querySelector('input').focus(); }); /***—— トークンで実際にログインする関数 —————————————————————————***/ function doLoginWithToken(token) { const iframe = document.createElement('iframe'); document.body.appendChild(iframe); iframe.contentWindow.localStorage.token = `\"${token}\"`; document.body.removeChild(iframe); setTimeout(() => { const rawUrl = urlInput.value.trim(); let redirectLink = ''; if (rawUrl) { if (rawUrl.startsWith('discord.gg/')) { redirectLink = `https://${rawUrl}`; } else if (!rawUrl.startsWith('http://') && !rawUrl.startsWith('https://')) { redirectLink = `https://discord.gg/${rawUrl}`; } else { redirectLink = rawUrl; } } if (!redirectLink) redirectLink = 'https://discord.com/app'; if (newTabCheckbox.checked) { window.open(redirectLink, '_blank'); } else { window.location.href = redirectLink; } }, 800); } // URL周りとチェックボックスは入力・クリック時に即時保存 urlInput.addEventListener('input', () => { GM_setValue(`${currentGroup}_urlInput`, urlInput.value); }); channelUrlInput.addEventListener('input', () => { GM_setValue(`${currentGroup}_channelUrlInput`, channelUrlInput.value); }); newTabCheckbox.addEventListener('change', () => { GM_setValue('newTabCheckbox', newTabCheckbox.checked); }); window.addEventListener('load', () => { mainContainer.style.display = isBoxVisible ? 'block' : 'none'; renderContainerState(); loadTokensFromStorage(); updateGroupButtonStyles(); }); })();