(!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}); });