let myName = localStorage.getItem('chat_username') || ''; let myRole = 'user'; // user | admin | mod | viewer let isBanned = false; let adminPW = localStorage.getItem('chat_admin_pw') || null; let banned = new Set(JSON.parse(localStorage.getItem('chat_banned') || '[]')); let viewers = new Set(JSON.parse(localStorage.getItem('chat_viewers') || '[]')); let mods = new Set(JSON.parse(localStorage.getItem('chat_mods') || '[]')); let online = new Map(); // name → {role, banned?, viewer?} // ────────────────────────────────────────────── function init() { if (!myName) { myName = prompt('ユーザー名を入力してください', 'Guest' + Math.floor(Math.random()*1000)); if (!myName || myName.trim()==='') myName = 'Guest'; localStorage.setItem('chat_username', myName); } mynameEl.textContent = myName; // 管理者チェック if (adminPW && confirm('管理者としてログインしますか?')) { const pw = prompt('管理者パスワードを入力'); if (pw === adminPW) { myRole = 'admin'; addSystem('管理者モードでログインしました'); } } // 自分の状態確認 if (banned.has(myName)) { isBanned = true; addSystem('あなたはBANされています(閲覧のみ)'); } else if (viewers.has(myName)) { myRole = 'viewer'; addSystem('閲覧専用モードです'); } else if (mods.has(myName)) { myRole = 'mod'; } input.disabled = isBanned || myRole==='viewer'; sendBtn.disabled = isBanned || myRole==='viewer'; // 参加通知 broadcast({t:'join', n:myName, r:myRole}); input.focus(); input.onkeydown = e => { if (e.key==='Enter') { e.preventDefault(); send(); }}; sendBtn.onclick = send; // 生存確認(30秒毎) setInterval(() => { if (!isBanned) broadcast({t:'hb', n:myName, r:myRole}); }, 30000); setInterval(updateUsers, 8000); } if (!adminPW) { const pw = prompt('初回:管理者パスワードを設定してください(空でスキップ可)'); if (pw && pw.trim()) { localStorage.setItem('chat_admin_pw', pw.trim()); adminPW = pw.trim(); alert('管理者パスワードを設定しました'); } } init(); // ────────────────────────────────────────────── function broadcast(obj) { channel.postMessage(obj); } function send() { let text = input.value.trim(); if (!text) return; if (text.startsWith('/')) { handleCommand(text); } else if (!isBanned && myRole !== 'viewer') { broadcast({t:'msg', n:myName, txt:text, time:new Date().toLocaleTimeString('ja-JP',{hour12:false})}); } input.value = ''; } function handleCommand(cmd) { const parts = cmd.slice(1).trim().split(/\s+/); const op = parts[0].toLowerCase(); const target = parts[1]; if (!target && !['delete'].includes(op)) { addSystem('使い方例: /ban ユーザー名 /mod ユーザー名 /viewer ユーザー名 /delete'); return; } const isAdmin = myRole==='admin'; const isMod = myRole==='admin' || myRole==='mod'; switch(op) { case 'ban': if (!isMod) return addSystem('権限がありません'); broadcast({t:'ban', target}); break; case 'viewer': if (!isMod) return addSystem('権限がありません'); broadcast({t:'viewer', target}); break; case 'mod': if (!isAdmin) return addSystem('管理者しか実行できません'); broadcast({t:'mod', target}); break; case 'delete': if (!isMod) return addSystem('権限がありません'); broadcast({t:'clear'}); break; default: addSystem('不明なコマンドです'); } } // ────────────────────────────────────────────── channel.onmessage = e => { const d = e.data; switch(d.t) { case 'msg': if (banned.has(d.n)) return; addMsg(d.n, d.txt, d.time, d.n === myName); break; case 'join': case 'hb': online.set(d.n, {r:d.r || 'user'}); updateUsers(); if (d.t==='join' && d.n !== myName) { addSystem(`${d.n} が参加しました`); } break; case 'ban': if (!['admin','mod'].includes(myRole)) return; banned.add(d.target); localStorage.setItem('chat_banned', JSON.stringify([...banned])); addSystem(`${d.target} をBANしました`); if (d.target === myName) { isBanned = true; input.disabled = sendBtn.disabled = true; addSystem('あなたはBANされました'); } updateUsers(); break; case 'viewer': if (!['admin','mod'].includes(myRole)) return; viewers.add(d.target); localStorage.setItem('chat_viewers', JSON.stringify([...viewers])); addSystem(`${d.target} を閲覧専用にしました`); updateUsers(); break; case 'mod': if (myRole !== 'admin') return; mods.add(d.target); localStorage.setItem('chat_mods', JSON.stringify([...mods])); addSystem(`${d.target} を仮管理者にしました`); updateUsers(); break; case 'clear': if (!['admin','mod'].includes(myRole)) return; messages.innerHTML = ''; addSystem('=== 管理者により全メッセージが削除されました ==='); break; } }; // ────────────────────────────────────────────── function addMsg(name, text, time, mine) { const div = document.createElement('div'); div.className = `msg ${mine ? 'mine' : 'other'} ${banned.has(name) ? 'banned' : ''}`; div.innerHTML = `${name} (${time})
${text.replace(/\n/g,'
')}`; messages.appendChild(div); messages.scrollTop = messages.scrollHeight; } function addSystem(text) { const div = document.createElement('div'); div.className = 'msg system'; div.textContent = text; messages.appendChild(div); messages.scrollTop = messages.scrollHeight; } function updateUsers() { userlist.innerHTML = ''; let cnt = 0; online.forEach((info, name) => { cnt++; const div = document.createElement('div'); div.className = 'user'; if (info.r === 'admin') div.classList.add('admin'); if (info.r === 'mod') div.classList.add('mod'); if (viewers.has(name)) div.classList.add('viewer'); if (banned.has(name)) div.classList.add('banned'); div.textContent = name; userlist.appendChild(div); }); status.textContent = `オンライン: ${cnt} 人`; } function logout() { if (confirm('ログアウトしますか?(名前がリセットされます)')) { localStorage.removeItem('chat_username'); location.reload(); } } // ページ離脱時に退出通知(ベストエフォート) window.addEventListener('beforeunload', () => { broadcast({t:'leave', n:myName}); });