#!/bin/bash
# ditch / public - disposable open chat
# curl -fsSL https://mirror.ditch.lyntric.de/ditch-public.sh | bash
# lyntric.de group - since 2025 - MIT

set -e
IDIR="/opt/ditch"
SVC="ditch"

[ "$EUID" -ne 0 ] && echo "run as root." && exit 1

echo "ditch / public - installing..."

# ── node.js ───────────────────────────────────────────────────────────────────
if ! command -v node &>/dev/null; then
  if command -v apt-get &>/dev/null; then
    curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
  elif command -v yum &>/dev/null; then
    curl -fsSL https://rpm.nodesource.com/setup_20.x | bash - && yum install -y nodejs
  elif command -v dnf &>/dev/null; then
    curl -fsSL https://rpm.nodesource.com/setup_20.x | bash - && dnf install -y nodejs
  elif command -v pacman &>/dev/null; then
    pacman -Sy --noconfirm nodejs npm
  else
    echo "install node.js manually then re-run." && exit 1
  fi
fi
command -v openssl &>/dev/null || { apt-get install -y openssl 2>/dev/null || yum install -y openssl 2>/dev/null || true; }

mkdir -p "$IDIR"
[ ! -f "$IDIR/messages.json" ] && echo '[]' > "$IDIR/messages.json"

# ── port config ───────────────────────────────────────────────────────────────
echo ""
read_port() {
  local label="$1" default="$2" varname="$3"
  while true; do
    printf "  %s [default: %s]: " "$label" "$default" > /dev/tty
    read INPUT < /dev/tty
    INPUT="${INPUT:-$default}"
    if [[ "$INPUT" =~ ^[0-9]+$ ]] && [ "$INPUT" -ge 1 ] && [ "$INPUT" -le 65535 ]; then
      eval "$varname=$INPUT"
      break
    fi
    echo "  invalid port. enter a number between 1 and 65535." > /dev/tty
  done
}

read_port "HTTP port " "80"   PORT_HTTP
read_port "HTTPS port" "443"  PORT_HTTPS

echo ""
echo "  ports: HTTP :${PORT_HTTP}  HTTPS :${PORT_HTTPS}"
echo ""

# write config
cat > "$IDIR/config.json" << CFGEOF
{"port_http":${PORT_HTTP},"port_https":${PORT_HTTPS}}
CFGEOF

# ── TLS cert ──────────────────────────────────────────────────────────────────
if [ ! -f "$IDIR/cert.pem" ]; then
  openssl req -x509 -newkey rsa:2048 -keyout "$IDIR/key.pem" -out "$IDIR/cert.pem" \
    -days 3650 -nodes -subj "/CN=ditch" 2>/dev/null
fi

# ── server.js ─────────────────────────────────────────────────────────────────
cat > "$IDIR/server.js" << 'EOF'
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');

const MSG_FILE = path.join(__dirname, 'messages.json');
const CFG_FILE = path.join(__dirname, 'config.json');
const MAX_MESSAGES = 200;
const MAX_MSG_LEN = 500;
const RATE_LIMIT_MS = 1000;

function loadConfig() {
  try {
    const c = JSON.parse(fs.readFileSync(CFG_FILE, 'utf8'));
    return {
      port_http:  parseInt(c.port_http)  || 80,
      port_https: parseInt(c.port_https) || 443,
    };
  } catch { return { port_http: 80, port_https: 443 }; }
}

const CFG = loadConfig();
const PORT_HTTP  = CFG.port_http;
const PORT_HTTPS = CFG.port_https;

const clients = new Set();
let online = 0;
let msgId = 0;
const rl = new Map();

const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const rip = r => (r.headers['x-forwarded-for'] || r.socket.remoteAddress || '').split(',')[0].trim();
const ok_rl = i => { const n=Date.now(),l=rl.get(i)||0; if(n-l<RATE_LIMIT_MS)return false; rl.set(i,n); return true; };
const load = () => { try { return JSON.parse(fs.readFileSync(MSG_FILE,'utf8')); } catch { return []; } };
const save = m => fs.writeFileSync(MSG_FILE, JSON.stringify(m));
const bcast = d => { const s=`data: ${JSON.stringify(d)}\n\n`; clients.forEach(r=>{ try{r.write(s)}catch{clients.delete(r)} }); };

const HTML = `<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>ditch</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700&family=Syne+Mono&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--w:#f5f4f0;--off:#eceae4;--bk:#0e0d0b;--ink:#1a1916;--mid:#7a7870;--faint:#c8c6be;--line:#e2e0d8;--bar:#f5f4f0;--acc:#d4522a}
html.dark{--w:#0e0d0b;--off:#161513;--bk:#f0efe9;--ink:#d8d6ce;--mid:#605e58;--faint:#3a3834;--line:#222120;--bar:#111010}
html,body{height:100%;background:var(--w);color:var(--ink);font-family:'Syne',sans-serif;font-size:14px;transition:background .2s,color .2s}
#wrap{display:flex;flex-direction:column;height:100%;max-width:680px;margin:0 auto;border-left:1px solid var(--line);border-right:1px solid var(--line)}
#hdr{display:flex;align-items:center;padding:0 24px;height:48px;border-bottom:1px solid var(--line);flex-shrink:0;gap:12px;background:var(--bar)}
.logo{font-family:'Syne Mono',monospace;font-size:14px;color:var(--bk)}
.logo em{color:var(--acc);font-style:normal}
.tag{font-family:'Syne Mono',monospace;font-size:10px;color:var(--faint);border:1px solid var(--line);padding:3px 8px;letter-spacing:.3px}
#onl{margin-left:auto;font-family:'Syne Mono',monospace;font-size:11px;color:var(--faint);display:flex;align-items:center;gap:6px}
#dmb{background:transparent;border:1px solid var(--line);color:var(--mid);width:28px;height:28px;cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:border-color .15s,color .15s;margin-left:10px}
#dmb:hover{border-color:var(--faint);color:var(--ink)}
.dot{width:6px;height:6px;border-radius:50%;background:var(--faint);flex-shrink:0}
.dot.live{background:#6abf6a;animation:blink 2.5s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}
#msgs{flex:1;overflow-y:auto;padding:16px 24px;display:flex;flex-direction:column;gap:2px;background:var(--w)}
#msgs::-webkit-scrollbar{width:3px}
#msgs::-webkit-scrollbar-thumb{background:var(--line)}
.msg{display:flex;gap:10px;align-items:flex-start;padding:4px 0}
.msg.new{animation:fi .15s ease}
@keyframes fi{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
.ts{font-family:'Syne Mono',monospace;font-size:10px;color:var(--faint);padding-top:3px;min-width:36px;flex-shrink:0}
.nn{font-family:'Syne Mono',monospace;font-size:11px;font-weight:700;color:var(--acc);min-width:88px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tx{font-size:14px;color:var(--ink);word-break:break-word;line-height:1.55}
.msg.sys .tx{color:var(--faint);font-size:12px;font-style:italic}
#bar{padding:12px 24px;border-top:1px solid var(--line);display:flex;gap:8px;flex-shrink:0;background:var(--bar)}
#ni{width:96px;background:transparent;border:1px solid var(--line);color:var(--ink);padding:9px 12px;font-family:'Syne Mono',monospace;font-size:12px;outline:none;flex-shrink:0;transition:border-color .15s}
#ni:focus{border-color:var(--faint)}
#mi{flex:1;background:transparent;border:1px solid var(--line);color:var(--ink);padding:9px 14px;font-family:'Syne',sans-serif;font-size:14px;outline:none;transition:border-color .15s}
#mi:focus{border-color:var(--faint)}
#mi::placeholder{color:var(--faint)}
#sb{background:var(--bk);color:var(--w);border:none;padding:9px 20px;font-family:'Syne Mono',monospace;font-size:12px;cursor:pointer;flex-shrink:0;transition:background .15s;letter-spacing:.3px}
#sb:hover{background:var(--acc)}
#sb:active{transform:scale(.98)}
#stbar{padding:4px 24px 8px;flex-shrink:0;font-family:'Syne Mono',monospace;font-size:10px;color:var(--faint);display:flex;align-items:center;gap:6px;background:var(--w)}
</style></head><body>
<div id="wrap">
  <div id="hdr">
    <div class="logo"><em>ditch</em> / chat</div>
    <div class="tag">public</div>
    <div id="onl"><span class="dot live"></span><span id="cnt">-</span></div>
    <button id="dmb" title="toggle dark mode">○</button>
  </div>
  <div id="msgs"></div>
  <div id="stbar"><span class="dot" id="sdot"></span><span id="stxt">connecting</span></div>
  <div id="bar">
    <input id="ni" maxlength="20" placeholder="name">
    <input id="mi" maxlength="500" placeholder="say something...">
    <button id="sb">send</button>
  </div>
</div>
<script>
const M=document.getElementById('msgs'),mi=document.getElementById('mi'),ni=document.getElementById('ni'),cnt=document.getElementById('cnt'),sdot=document.getElementById('sdot'),stxt=document.getElementById('stxt'),dmb=document.getElementById('dmb'),html=document.documentElement;
const setDark=d=>{html.classList.toggle('dark',d);dmb.textContent=d?'●':'○';localStorage.setItem('ditch_dark',d?'1':'0')};
setDark(localStorage.getItem('ditch_dark')==='1');
dmb.addEventListener('click',()=>setDark(!html.classList.contains('dark')));
ni.value=localStorage.getItem('ditch_nick')||'';
ni.addEventListener('input',()=>localStorage.setItem('ditch_nick',ni.value));
const ts=ms=>{const d=new Date(ms);return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')};
const addMsg=(m,n)=>{const d=document.createElement('div');d.className='msg'+(m.sys?' sys':'')+(n?' new':'');d.innerHTML='<span class="ts">'+ts(m.t)+'</span><span class="nn">'+m.n+'</span><span class="tx">'+m.m+'</span>';M.appendChild(d);M.scrollTop=M.scrollHeight};
fetch('/api/messages').then(r=>r.json()).then(ms=>ms.forEach(m=>addMsg(m,false))).catch(()=>{});
const conn=()=>{const es=new EventSource('/api/stream');sdot.className='dot live';stxt.textContent='live';es.onmessage=e=>{try{const d=JSON.parse(e.data);if(d.type==='msg')addMsg(d,true);if(d.type==='online')cnt.textContent=d.count+' online'}catch{}};es.onerror=()=>{sdot.className='dot';stxt.textContent='reconnecting';es.close();setTimeout(conn,2500)}};
conn();
const send=async()=>{const t=mi.value.trim();if(!t)return;const n=(ni.value.trim()||'anon').slice(0,20);mi.value='';try{await fetch('/api/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({n,m:t})})}catch{mi.value=t}};
document.getElementById('sb').addEventListener('click',send);
mi.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
</script></body></html>`;

function handle(req, res) {
  const u = new URL(req.url, `http://${req.headers.host}`);
  if (u.pathname === '/') { res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'}); return res.end(HTML); }
  if (u.pathname === '/api/messages') { res.writeHead(200,{'Content-Type':'application/json'}); return res.end(JSON.stringify(load())); }
  if (u.pathname === '/api/online') { res.writeHead(200,{'Content-Type':'application/json'}); return res.end(JSON.stringify({count:online})); }
  if (u.pathname === '/api/send' && req.method === 'POST') {
    let b=''; req.on('data',d=>b+=d); req.on('end',()=>{
      try {
        if (!ok_rl(rip(req))) { res.writeHead(429); return res.end(); }
        const {n,m} = JSON.parse(b);
        const msg = { id:++msgId, t:Date.now(), n:esc((n||'anon').slice(0,20)), m:esc((m||'').slice(0,MAX_MSG_LEN)) };
        if (!msg.m) { res.writeHead(400); return res.end(); }
        const msgs = load(); msgs.push(msg);
        if (msgs.length > MAX_MESSAGES) msgs.splice(0, msgs.length - MAX_MESSAGES);
        save(msgs); bcast({type:'msg',...msg}); res.writeHead(200); res.end('ok');
      } catch { res.writeHead(400); res.end(); }
    }); return;
  }
  if (u.pathname === '/api/stream') {
    res.writeHead(200,{'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive'});
    res.write(`data: ${JSON.stringify({type:'init'})}\n\n`);
    clients.add(res); online++;
    bcast({type:'online',count:online});
    req.on('close',()=>{ clients.delete(res); online=Math.max(0,online-1); bcast({type:'online',count:online}); });
    return;
  }
  res.writeHead(404); res.end();
}

http.createServer(handle).listen(PORT_HTTP, () => console.log(`[ditch] http  :${PORT_HTTP}`));
try {
  https.createServer({ key:fs.readFileSync(path.join(__dirname,'key.pem')), cert:fs.readFileSync(path.join(__dirname,'cert.pem')) }, handle)
    .listen(PORT_HTTPS, () => console.log(`[ditch] https :${PORT_HTTPS}`));
} catch(e) { console.warn('[ditch] https not available:', e.message); }
setInterval(()=>{},1<<30);
EOF

# ── systemd ───────────────────────────────────────────────────────────────────
cat > "/etc/systemd/system/${SVC}.service" << SVCEOF
[Unit]
Description=ditch public chat
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=${IDIR}
ExecStart=$(command -v node) ${IDIR}/server.js
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
SVCEOF

systemctl daemon-reload
systemctl enable "$SVC" 2>/dev/null
systemctl restart "$SVC"

IP=$(hostname -I | awk '{print $1}')
echo "ditch / public is live."
echo ""
if [ "$PORT_HTTP" = "80" ]; then
  echo "  http://${IP}"
else
  echo "  http://${IP}:${PORT_HTTP}"
fi
if [ "$PORT_HTTPS" = "443" ]; then
  echo "  https://${IP}  (self-signed cert)"
else
  echo "  https://${IP}:${PORT_HTTPS}  (self-signed cert)"
fi
echo ""
echo "  logs:    journalctl -u ditch -f"
echo "  stop:    systemctl stop ditch"
echo "  remove:  systemctl disable ditch && rm -rf /opt/ditch"