| const logger = require("koa-logger"); |
| const responseTime = require("koa-response-time"); |
| const bodyParser = require("koa-bodyparser"); |
| const Router = require("koa-router"); |
| const Koa = require("koa"); |
| const os = require("os"); |
| const fs = require("fs"); |
| const https = require("https"); |
| const path = require("path"); |
|
|
| const app = new Koa(); |
|
|
| |
| const requiredAssets = [ |
| { |
| url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_02.png", |
| dest: "assets/pattern_02.png", |
| }, |
| { |
| url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_ny.png", |
| dest: "assets/pattern_ny.png", |
| }, |
| { |
| url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_ny_old.png", |
| dest: "assets/pattern_ny_old.png", |
| }, |
| { |
| url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-apple-image.json", |
| dest: "assets/emoji/emoji-apple-image.json", |
| }, |
| { |
| url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-google-image.json", |
| dest: "assets/emoji/emoji-google-image.json", |
| }, |
| { |
| url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-joypixels-image.json", |
| dest: "assets/emoji/emoji-joypixels-image.json", |
| }, |
| { |
| url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-twitter-image.json", |
| dest: "assets/emoji/emoji-twitter-image.json", |
| }, |
| ]; |
|
|
| |
| function downloadFile(url, dest) { |
| return new Promise((resolve, reject) => { |
| const dir = path.dirname(dest); |
| if (!fs.existsSync(dir)) { |
| fs.mkdirSync(dir, { recursive: true }); |
| } |
|
|
| if (fs.existsSync(dest)) { |
| console.log(`β ${dest} already exists`); |
| return resolve(); |
| } |
|
|
| const file = fs.createWriteStream(dest); |
| console.log(`β¬ Downloading ${path.basename(dest)}...`); |
|
|
| https |
| .get(url, (response) => { |
| if (response.statusCode === 301 || response.statusCode === 302) { |
| return downloadFile(response.headers.location, dest) |
| .then(resolve) |
| .catch(reject); |
| } |
|
|
| if (response.statusCode !== 200) { |
| reject( |
| new Error(`Failed to download ${url}: ${response.statusCode}`) |
| ); |
| return; |
| } |
|
|
| response.pipe(file); |
|
|
| file.on("finish", () => { |
| file.close(); |
| console.log(`β
Downloaded ${dest}`); |
| resolve(); |
| }); |
| }) |
| .on("error", (err) => { |
| fs.unlink(dest, () => {}); |
| reject(err); |
| }); |
|
|
| file.on("error", (err) => { |
| fs.unlink(dest, () => {}); |
| reject(err); |
| }); |
| }); |
| } |
|
|
| |
| async function ensureAssets() { |
| console.log("\nββββββββββββββββββββββββββββββββββββββββ"); |
| console.log("π Checking required assets..."); |
| console.log("ββββββββββββββββββββββββββββββββββββββββ\n"); |
|
|
| try { |
| for (const asset of requiredAssets) { |
| await downloadFile(asset.url, asset.dest); |
| } |
| console.log("\n⨠All assets ready!\n"); |
| } catch (err) { |
| console.error("\nβ Error downloading assets:"); |
| console.error(err.message); |
| throw err; |
| } |
| } |
|
|
| |
| app.use(async (ctx, next) => { |
| const start = Date.now(); |
|
|
| |
| console.log("\nββββββββββββββββββββββββββββββββββββββββ"); |
| console.log("π¨ INCOMING REQUEST"); |
| console.log("Time:", new Date().toISOString()); |
| console.log("Method:", ctx.method); |
| console.log("URL:", ctx.url); |
|
|
| if (ctx.request.body && Object.keys(ctx.request.body).length > 0) { |
| console.log("Body:", JSON.stringify(ctx.request.body, null, 2)); |
| } |
|
|
| try { |
| await next(); |
|
|
| |
| const ms = Date.now() - start; |
| console.log("\nβ
RESPONSE SUCCESS"); |
| console.log("Status:", ctx.status); |
| console.log("Time:", ms + "ms"); |
| console.log("ββββββββββββββββββββββββββββββββββββββββ\n"); |
| } catch (err) { |
| const ms = Date.now() - start; |
|
|
| |
| console.log("\nβ ERROR OCCURRED"); |
| console.log("Time:", new Date().toISOString()); |
| console.log("Duration:", ms + "ms"); |
| console.log("Method:", ctx.method); |
| console.log("URL:", ctx.url); |
| console.log("Status:", err.status || err.statusCode || 500); |
| console.log("Error Name:", err.name); |
| console.log("Error Message:", err.message); |
|
|
| if (err.errors) { |
| console.log("Validation Errors:", JSON.stringify(err.errors, null, 2)); |
| } |
|
|
| console.log("\nπ STACK TRACE:"); |
| console.log(err.stack); |
| console.log("ββββββββββββββββββββββββββββββββββββββββ\n"); |
|
|
| |
| ctx.status = err.status || err.statusCode || 500; |
| ctx.body = { |
| success: false, |
| error: { |
| message: err.message || "Internal Server Error", |
| status: ctx.status, |
| timestamp: new Date().toISOString(), |
| ...(err.errors && { details: err.errors }), |
| }, |
| }; |
|
|
| |
| ctx.app.emit("error", err, ctx); |
| } |
| }); |
|
|
| app.use(logger()); |
| app.use(responseTime()); |
| app.use( |
| bodyParser({ |
| onerror: (err, ctx) => { |
| console.error("\nβ οΈ BODY PARSER ERROR"); |
| console.error("Message:", err.message); |
| console.error("Stack:", err.stack); |
| console.error("ββββββββββββββββββββββββββββββββββββββββ\n"); |
| ctx.throw(422, "Invalid JSON body"); |
| }, |
| }) |
| ); |
|
|
| |
| app.use(async (ctx, next) => { |
| await next(); |
|
|
| |
| if (ctx.status >= 400) { |
| console.log("\nβ οΈ ERROR RESPONSE BODY:"); |
| console.log(JSON.stringify(ctx.body, null, 2)); |
| console.log("ββββββββββββββββββββββββββββββββββββββββ\n"); |
| } |
| }); |
|
|
| app.use(require("./helpers").helpersApi); |
|
|
| const route = new Router(); |
| const routes = require("./routes"); |
|
|
| |
| function formatBytes(bytes) { |
| const sizes = ["B", "KB", "MB", "GB", "TB"]; |
| if (bytes === 0) return "0 B"; |
| const i = Math.floor(Math.log(bytes) / Math.log(1024)); |
| return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`; |
| } |
|
|
| function formatUptime(seconds) { |
| const days = Math.floor(seconds / (24 * 3600)); |
| seconds %= 24 * 3600; |
| const hours = Math.floor(seconds / 3600); |
| seconds %= 3600; |
| const minutes = Math.floor(seconds / 60); |
| seconds = Math.floor(seconds % 60); |
|
|
| const parts = []; |
| if (days) parts.push(`${days} day${days > 1 ? "s" : ""}`); |
| if (hours) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`); |
| if (minutes) parts.push(`${minutes} minute${minutes > 1 ? "s" : ""}`); |
| if (seconds || parts.length === 0) |
| parts.push(`${seconds} second${seconds > 1 ? "s" : ""}`); |
|
|
| return parts.join(" "); |
| } |
|
|
| route.get(["/", "/ping"], async (ctx) => { |
| const totalMem = os.totalmem(); |
| const freeMem = os.freemem(); |
| const usedMem = totalMem - freeMem; |
| const usagePercent = ((usedMem / totalMem) * 100).toFixed(2); |
|
|
| const info = { |
| hostname: os.hostname(), |
| platform: os.platform(), |
| arch: os.arch(), |
| uptime: formatUptime(os.uptime()), |
| cpu: { |
| model: os.cpus()[0].model, |
| cores: os.cpus().length, |
| }, |
| memory: { |
| total: formatBytes(totalMem), |
| used: formatBytes(usedMem), |
| free: formatBytes(freeMem), |
| usage: `${usagePercent}%`, |
| }, |
| }; |
|
|
| ctx.type = "application/json"; |
| ctx.body = JSON.stringify(info, null, 2); |
| }); |
|
|
| route.use("/*", routes.routeApi.routes()); |
| app.use(route.routes()); |
|
|
| |
| app.on("error", (err, ctx) => { |
| |
| |
| if (process.env.DEBUG) { |
| console.log("π Additional error context:", { |
| url: ctx.url, |
| method: ctx.method, |
| status: ctx.status, |
| headers: ctx.headers, |
| }); |
| } |
| }); |
|
|
| const port = process.env.PORT || 7860; |
|
|
| |
| async function startServer() { |
| try { |
| |
| await ensureAssets(); |
|
|
| |
| app.listen(port, () => { |
| console.log("ββββββββββββββββββββββββββββββββββββββββ"); |
| console.log("β
Server started successfully"); |
| console.log("π Listening on localhost, port", port); |
| console.log("π Health check: http://localhost:" + port + "/ping"); |
| console.log("π Debug mode:", process.env.DEBUG ? "ON" : "OFF"); |
| console.log("ββββββββββββββββββββββββββββββββββββββββ\n"); |
| }); |
| } catch (err) { |
| console.error("β Failed to start server:", err.message); |
| process.exit(1); |
| } |
| } |
|
|
| |
| startServer(); |
|
|
| |
| process.on("unhandledRejection", (reason, promise) => { |
| console.error("\nπ₯ Unhandled Rejection at:", promise); |
| console.error("Reason:", reason); |
| console.error("ββββββββββββββββββββββββββββββββββββββββ\n"); |
| }); |
|
|
| |
| process.on("uncaughtException", (err) => { |
| console.error("\nπ₯ Uncaught Exception:"); |
| console.error(err); |
| console.error("ββββββββββββββββββββββββββββββββββββββββ\n"); |
| process.exit(1); |
| }); |
|
|