Spaces:
Sleeping
Sleeping
File size: 9,084 Bytes
6751945 746dbde 6751945 746dbde 6751945 746dbde 6751945 746dbde 6751945 a00f3fc 6751945 746dbde 937d9e1 a00f3fc 937d9e1 746dbde 937d9e1 746dbde 937d9e1 746dbde 937d9e1 746dbde 937d9e1 746dbde 937d9e1 746dbde a00f3fc 746dbde a00f3fc 746dbde a00f3fc 746dbde a00f3fc 746dbde a00f3fc 746dbde d33c479 746dbde 937d9e1 746dbde 6751945 746dbde 937d9e1 746dbde 6751945 746dbde 6751945 746dbde a00f3fc 746dbde 6751945 746dbde 6751945 746dbde 6751945 a00f3fc 746dbde 937d9e1 746dbde 6751945 a00f3fc 746dbde a00f3fc 937d9e1 746dbde 6751945 a00f3fc 746dbde 6751945 746dbde 6751945 746dbde | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | #!/usr/bin/env node
/**
* SocialCrypt Server
* ===================
*
* Dumb JSONL blob store for encrypted social messaging.
* Works everywhere: bare metal, Docker, HuggingFace Spaces,
* Railway, Fly.io, Render, Glitch, etc.
*
* GET / β Web dashboard
* GET /blobs.jsonl β All blobs (JSONL format)
* POST /submit β Append { id, blob } (server sets ts)
* GET /stats β Message count & size
* GET /health β Health check
*
* Storage: Uses /data on HuggingFace Spaces (persistent volume),
* falls back to local ./data everywhere else.
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
// βββ Configuration βββββββββββββββββββββββββββββββββββββββββββββββββββ
const PORT = parseInt(process.env.PORT) || 7860;
const DATA_DIR = fs.existsSync('/data') ? '/data' : path.join(__dirname, 'data');
const BLOBS_FILE = path.join(DATA_DIR, 'blobs.jsonl');
// βββ Initialize ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
console.log(`π Data directory: ${DATA_DIR}`);
// βββ Helper: get stats ββββββββββββββββββββββββββββββββββββββββββββββ
function getStats() {
if (!fs.existsSync(BLOBS_FILE)) return { messages: 0, size_bytes: 0, size_human: '0 B' };
const stat = fs.statSync(BLOBS_FILE);
const content = fs.readFileSync(BLOBS_FILE, 'utf-8').trim();
const lines = content ? content.split('\n').filter(Boolean) : [];
const sizeKB = (stat.size / 1024).toFixed(1);
return {
messages: lines.length,
size_bytes: stat.size,
size_human: stat.size > 1048576 ? (stat.size / 1048576).toFixed(1) + ' MB' : sizeKB + ' KB'
};
}
// βββ HTTP Server βββββββββββββββββββββββββββββββββββββββββββββββββββββ
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, HEAD');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') { res.writeHead(200); return res.end(); }
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
const p = url.pathname;
// GET / β Dashboard
if (req.method === 'GET' && p === '/') {
const s = getStats();
const rows = [];
if (s.messages > 0) {
const lines = fs.readFileSync(BLOBS_FILE, 'utf-8').trim().split('\n').filter(Boolean).reverse().slice(0, 50);
for (const line of lines) {
try {
const e = JSON.parse(line);
rows.push(`<tr><td>${(e.id||'?').substring(0,16)}...</td><td>${e.ts ? new Date(e.ts).toLocaleString() : '?'}</td><td>${e.blob ? (e.blob.length/1024).toFixed(1)+' KB' : '?'}</td></tr>`);
} catch {}
}
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(`<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>π SocialCrypt Server</title>
<style>
:root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#e6edf3;--dim:#8b949e;--accent:#58a6ff}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;line-height:1.6;padding:20px}
.container{max-width:800px;margin:0 auto}
h1{text-align:center;padding:40px 0 20px;font-size:2.5rem;color:var(--accent)}
h2{font-size:1.2rem;margin-bottom:16px}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;margin-bottom:16px}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.box{text-align:center;padding:16px;background:var(--bg);border:1px solid var(--border);border-radius:8px}
.box .n{font-size:2rem;font-weight:700;color:var(--accent)}
.box .l{font-size:0.82rem;color:var(--dim);margin-top:4px}
table{width:100%;border-collapse:collapse}
th{text-align:left;padding:10px 12px;border-bottom:2px solid var(--border);color:var(--dim);font-size:0.82rem}
td{padding:10px 12px;border-bottom:1px solid var(--border);font-family:monospace;font-size:0.85rem}
tr:hover td{background:rgba(255,255,255,0.03)}
.footer{text-align:center;padding:24px;color:var(--dim);font-size:0.85rem}
</style></head>
<body><div class="container">
<h1>π SocialCrypt</h1>
<div class="card"><h2>π Stats</h2>
<div class="grid">
<div class="box"><div class="n">${s.messages}</div><div class="l">Messages</div></div>
<div class="box"><div class="n">${s.size_human}</div><div class="l">Size</div></div>
<div class="box"><div class="n">${DATA_DIR === '/data' ? '/data' : './data'}</div><div class="l">Storage</div></div>
</div></div>
<div class="card"><h2>π‘ Endpoints</h2>
<p style="font-family:monospace;font-size:0.9rem;margin:4px 0">GET /blobs.jsonl β Read all blobs</p>
<p style="font-family:monospace;font-size:0.9rem;margin:4px 0">POST /submit β Submit {id, blob}</p>
<p style="font-family:monospace;font-size:0.9rem;margin:4px 0">GET /stats β JSON stats</p>
<p style="font-family:monospace;font-size:0.9rem;margin:4px 0">GET /health β Health check</p>
</div>
<div class="card"><h2>π¬ Messages ${s.messages > 0 ? '('+Math.min(s.messages,50)+' of '+s.messages+')' : ''}</h2>
${s.messages > 0 ? `<div style="overflow-x:auto"><table><thead><tr><th>Recipient</th><th>Time</th><th>Size</th></tr></thead><tbody>${rows.join('')}</tbody></table></div>` : '<p style="text-align:center;padding:40px;color:var(--dim)">π No messages yet</p>'}
</div>
<div class="footer">SocialCrypt β Encrypted Social Messaging</div>
</div></body></html>`);
}
// GET /blobs.jsonl β Stream blobs
if (req.method === 'GET' && p === '/blobs.jsonl') {
const s = getStats();
res.writeHead(200, { 'Content-Type': 'application/x-ndjson', 'X-Total-Messages': String(s.messages) });
if (fs.existsSync(BLOBS_FILE)) return fs.createReadStream(BLOBS_FILE).on('error', () => res.end()).pipe(res);
return res.end('');
}
// POST /submit β Append blob
if (req.method === 'POST' && p === '/submit') {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
try {
const data = JSON.parse(body);
if (!data.id || !data.blob) { res.writeHead(400); return res.end(JSON.stringify({error:'Missing id or blob'})); }
const line = JSON.stringify({ id: data.id, ts: Date.now(), blob: data.blob }) + '\n';
if (!fs.existsSync(path.dirname(BLOBS_FILE))) fs.mkdirSync(path.dirname(BLOBS_FILE), { recursive: true });
fs.appendFileSync(BLOBS_FILE, line, 'utf-8');
console.log(`π¨ Stored for ${data.id.substring(0,16)}... | Total: ${getStats().messages}`);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
} catch (e) { res.writeHead(400); res.end(JSON.stringify({error:'Invalid JSON: '+e.message})); }
});
return;
}
// GET /stats
if (req.method === 'GET' && p === '/stats') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ ...getStats(), data_dir: DATA_DIR, port: PORT }));
}
// GET /health
if (req.method === 'GET' && p === '/health') {
let writable = false;
try { fs.accessSync(path.dirname(BLOBS_FILE), fs.constants.W_OK); writable = true; } catch {}
res.writeHead(writable ? 200 : 503, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ status: writable ? 'ok' : 'degraded', messages: getStats().messages }));
}
// 404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('SocialCrypt β try /blobs.jsonl, /submit, /stats, /health');
});
// βββ Startup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
server.listen(PORT, '0.0.0.0', () => {
console.log(`ββββββββββββββββββββββββββββββββββββ`);
console.log(`β π SocialCrypt Server β`);
console.log(`β π‘ http://0.0.0.0:${String(PORT).padEnd(5)} β`);
console.log(`β πΎ ${String(DATA_DIR).padEnd(25)}β`);
console.log(`β π¬ ${String(getStats().messages).padStart(5)} messages β`);
console.log(`ββββββββββββββββββββββββββββββββββββ`);
});
process.on('SIGTERM', () => { server.close(() => process.exit(0)); setTimeout(() => process.exit(1), 5000); });
process.on('SIGINT', () => { server.close(() => process.exit(0)); setTimeout(() => process.exit(1), 5000); }); |