#!/usr/bin/env node 'use strict'; const http = require('http'); const fs = require('fs'); const path = require('path'); const { URL } = require('url'); const PORT = Number(process.env.PORT || 3000); const PUBLIC_DIR = path.join(__dirname, 'public'); let nextTweetId = 4; let nextReplyId = 1; const tweets = [ { id: 1, author: 'whetstone', text: 'Twitter clone local demo is live.', likes: 3, createdAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(), replies: [] }, { id: 2, author: 'editor-bot', text: 'You can post, like, and reply in this demo timeline.', likes: 1, createdAt: new Date(Date.now() - 1000 * 60 * 20).toISOString(), replies: [] }, { id: 3, author: 'bill', text: 'Running locally with zero external dependencies.', likes: 2, createdAt: new Date(Date.now() - 1000 * 60 * 10).toISOString(), replies: [] } ]; function sendJson(res, statusCode, payload) { const body = JSON.stringify(payload); res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body) }); res.end(body); } function sendText(res, statusCode, text) { res.writeHead(statusCode, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': Buffer.byteLength(text) }); res.end(text); } function parseBody(req) { return new Promise((resolve, reject) => { let raw = ''; req.on('data', chunk => { raw += chunk; if (raw.length > 1024 * 1024) { reject(new Error('payload_too_large')); } }); req.on('end', () => { if (!raw) { resolve({}); return; } try { resolve(JSON.parse(raw)); } catch { reject(new Error('invalid_json')); } }); req.on('error', reject); }); } function getTweetById(id) { return tweets.find(t => t.id === id) || null; } function listTweets() { return [...tweets].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } async function handleApi(req, res, pathname) { if (req.method === 'GET' && pathname === '/api/health') { sendJson(res, 200, { ok: true, service: 'twitter-clone-local' }); return; } if (req.method === 'GET' && pathname === '/api/tweets') { sendJson(res, 200, { tweets: listTweets() }); return; } if (req.method === 'POST' && pathname === '/api/tweets') { try { const body = await parseBody(req); const author = String(body.author || '').trim(); const text = String(body.text || '').trim(); if (!author || !text) { sendJson(res, 400, { error: 'author_and_text_required' }); return; } const tweet = { id: nextTweetId++, author, text, likes: 0, createdAt: new Date().toISOString(), replies: [] }; tweets.push(tweet); sendJson(res, 201, { tweet }); } catch (err) { sendJson(res, 400, { error: err.message || 'invalid_request' }); } return; } const likeMatch = pathname.match(/^\/api\/tweets\/(\d+)\/like$/); if (req.method === 'POST' && likeMatch) { const id = Number(likeMatch[1]); const tweet = getTweetById(id); if (!tweet) { sendJson(res, 404, { error: 'tweet_not_found' }); return; } tweet.likes += 1; sendJson(res, 200, { tweet }); return; } const replyMatch = pathname.match(/^\/api\/tweets\/(\d+)\/reply$/); if (req.method === 'POST' && replyMatch) { const id = Number(replyMatch[1]); const tweet = getTweetById(id); if (!tweet) { sendJson(res, 404, { error: 'tweet_not_found' }); return; } try { const body = await parseBody(req); const author = String(body.author || '').trim(); const text = String(body.text || '').trim(); if (!author || !text) { sendJson(res, 400, { error: 'author_and_text_required' }); return; } const reply = { id: nextReplyId++, author, text, createdAt: new Date().toISOString() }; tweet.replies.push(reply); sendJson(res, 201, { tweet, reply }); } catch (err) { sendJson(res, 400, { error: err.message || 'invalid_request' }); } return; } sendJson(res, 404, { error: 'not_found' }); } function contentTypeFor(filePath) { const ext = path.extname(filePath).toLowerCase(); if (ext === '.html') return 'text/html; charset=utf-8'; if (ext === '.css') return 'text/css; charset=utf-8'; if (ext === '.js') return 'application/javascript; charset=utf-8'; if (ext === '.json') return 'application/json; charset=utf-8'; return 'text/plain; charset=utf-8'; } function safeFilePath(pathname) { const cleanPath = pathname === '/' ? '/index.html' : pathname; const abs = path.join(PUBLIC_DIR, path.normalize(cleanPath)); if (!abs.startsWith(PUBLIC_DIR)) return null; return abs; } function serveStatic(res, pathname) { const abs = safeFilePath(pathname); if (!abs) { sendText(res, 403, 'forbidden'); return; } fs.readFile(abs, (err, data) => { if (err) { if (err.code === 'ENOENT') { sendText(res, 404, 'not found'); } else { sendText(res, 500, 'internal error'); } return; } res.writeHead(200, { 'Content-Type': contentTypeFor(abs), 'Content-Length': data.length }); res.end(data); }); } const server = http.createServer(async (req, res) => { const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`); const pathname = parsedUrl.pathname; if (pathname.startsWith('/api/')) { await handleApi(req, res, pathname); return; } serveStatic(res, pathname); }); server.listen(PORT, () => { process.stdout.write(`twitter_clone_local listening on http://localhost:${PORT}\n`); });