import cfg from "./inc/config.mjs"; import db from "./inc/sql.mjs"; import lib from "./inc/lib.mjs"; import cuffeo from "cuffeo"; import fs from "fs"; import path from "path"; import { execSync } from "child_process"; import { getMotd, setMotd } from "./inc/motd.mjs"; import { getAboutText, setAboutText, getRulesText, setRulesText, getTermsText, setTermsText } from "./inc/page_texts.mjs"; import flummpress from "flummpress"; import { handleUpload } from "./upload_handler.mjs"; import { handleAvatarUpload, handleAvatarDelete } from "./avatar_handler.mjs"; import { handleRethumbUpload } from "./rethumb_handler.mjs"; import { handleMemeUpload } from "./meme_upload_handler.mjs"; import { handleEmojiUpload } from "./emoji_upload_handler.mjs"; import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs"; import { handleMetaExtract } from "./meta_extract_handler.mjs"; import { handleMetaStrip } from "./meta_strip_handler.mjs"; import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs"; import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs"; import { createI18n } from "./inc/i18n.mjs"; import security from "./inc/security.mjs"; import { createRequire } from 'module'; const _require = createRequire(import.meta.url); // ─── Private Society Gate ──────────────────────────────────────────────────── // Powered by the cloudflare-error-page package. // Each request gets a fresh page with a unique Ray ID + current UTC timestamp. // // Customise the visible text and status icons here: const gateOptions = { title: 'Bad Gateway', error_code: '502', what_happened: "There is an internal server error on Cloudflare's network.", // what_can_i_do is injected dynamically below (Sign in button) error_source: 'host', browser_status: { status: 'ok', location: 'You', name: 'Browser', status_text: 'Working', }, cloudflare_status: { status: 'ok', location: 'Frankfurt', name: 'Cloudflare', status_text: 'Working', }, host_status: { status: 'error', location: 'Website', name: 'Host', status_text: 'Error', }, more_information: { hidden: false, text: 'cloudflare.com', link: '', for: 'more information', }, perf_sec_by: { text: '', link: '' }, }; // Fallback when the package isn't installed const nginx502Fallback = `
const gateLoginInjection = `
`; // Text injected into the "What can I do?" section const gateSignInButton = `You know what to do.`; let _cfRender = null; try { _cfRender = _require('cloudflare-error-page').render; console.log('[BOOT] cloudflare-error-page loaded — gate pages generated dynamically per request'); } catch (e) { console.warn('[BOOT] cloudflare-error-page not installed, falling back to plain nginx 502:', e.message); } // Called on every gated request — produces a fresh page with unique Ray ID + timestamp // Accepts an optional request object to inject the visitor's real IP into the footer reveal. function buildGatePage(req) { if (!_cfRender) return nginx502Fallback; let html = _cfRender({ ...gateOptions, what_can_i_do: gateSignInButton }); // Inject real visitor IP into the footer "Your IP: [Click to reveal]" span if (req) { const visitorIp = security.getRealIP(req); html = html.replace( '', `` ); } // Make the second 'a' in "Bad Gateway" a secret click trigger html = html.replace( 'Bad Gateway', 'Bad Gateway' ); html = html.replace('', gateLoginInjection + '\n'); return html; } // nginx502 === null signals "dynamic cloudflare mode" to the middleware below const nginx502 = (cfg.websrv.private_society && cfg.websrv.private_society_gate === 'cloudflare') ? null : nginx502Fallback; if (nginx502 === null) console.log('[BOOT] Private society gate: Cloudflare dynamic mode'); // ───────────────────────────────────────────────────────────────────────────── const origLog = console.log; console.log = function(...args) { if (process.env.NODE_ENV === 'production') { if (typeof args[0] === 'string' && args[0].includes('[BOOT]')) { origLog.apply(console, args); } return; } origLog.apply(console, args); }; const logCrash = (type, err) => { const timestamp = new Date().toISOString(); const errMsg = err instanceof Error ? `${err.stack || err.message}` : JSON.stringify(err); const logEntry = `[${timestamp}] ${type}: ${errMsg}\n\n`; console.error(`[CRASH] ${type}:`, err); try { if (!fs.existsSync(cfg.paths.logs)) { fs.mkdirSync(cfg.paths.logs, { recursive: true }); } fs.appendFileSync(path.join(cfg.paths.logs, 'crash.log'), logEntry); } catch (e) { console.error('Failed to write to crash.log:', e); } }; process.on('unhandledRejection', err => { if (err && err.code === 'ERR_HTTP_HEADERS_SENT') return; logCrash('Unhandled Rejection', err); if (process.env.NODE_ENV === 'production') { // In production, we might want to restart, but let's see if we can stay alive // throw err; } }); process.on('uncaughtException', err => { logCrash('Uncaught Exception', err); process.exit(1); }); (async () => { const self = { _trigger: new Map(), trigger: function trigger(args) { this.call = args.call; this.help = args.help || false; this.level = args.level || 0; this.name = args.name; this.active = args.hasOwnProperty("active") ? args.active : true; this.clients = args.clients || ["irc", "tg", "slack", "matrix"]; this.f = args.f; }, bot: await new cuffeo(cfg.clients) }; // Ensure storage directories exist const initDirs = [ cfg.paths.a, cfg.paths.b, cfg.paths.t, cfg.paths.ca, cfg.paths.emojis, cfg.paths.memes, cfg.paths.tmp, cfg.paths.logs, path.join(cfg.paths.pending, 'b'), path.join(cfg.paths.pending, 't'), path.join(cfg.paths.pending, 'ca'), path.join(cfg.paths.deleted, 'b'), path.join(cfg.paths.deleted, 't'), path.join(cfg.paths.deleted, 'ca') ]; for (const dir of initDirs) { if (!fs.existsSync(dir)) { console.log(`[BOOT] Creating directory: ${dir}`); fs.mkdirSync(dir, { recursive: true }); } } const timeout = 10000; // 10s module load timeout // Ensure clients are resolved (Safe-guard for Docker/Node versions) if (self.bot.clients) { self.bot.clients = await Promise.all(self.bot.clients); } console.time("loading"); const modules = { events: (await fs.promises.readdir("./src/inc/events")).filter(f => f.endsWith(".mjs")), trigger: (await fs.promises.readdir("./src/inc/trigger")).filter(f => f.endsWith(".mjs")) }; console.timeLog("loading", "directories"); const blah = (await Promise.all(Object.entries(modules).map(async ([dir, mods]) => ({ [dir]: (await Promise.all(mods.map(async mod => { const res = await Promise.race([ (await import(`./inc/${dir}/${mod}`)).default(self), new Promise((_, rej) => setTimeout(() => rej(new Error(`Module loading timed out: ${dir}/${mod}`)), timeout)) ]); console.timeLog("loading", `${dir}/${mod}`); return res; }))).flat(2) })))).reduce((a, b) => ({ ...a, ...b })); blah.events.forEach(event => { console.timeLog("loading", `registering event > ${event.name}`); self.bot.on(event.listener, event.f); }); blah.trigger.forEach(trigger => { console.timeLog("loading", `registering trigger > ${trigger.name}`); self._trigger.set(trigger.name, new self.trigger(trigger)); }); // Initial halls cache (only if halls are enabled) if (cfg.websrv.halls_enabled !== false) { await updateHallsCache(); } // Log feature flags console.log(`[BOOT] Halls: ${cfg.websrv.halls_enabled !== false ? 'ENABLED' : 'DISABLED'}`); console.log(`[BOOT] UserHalls: ${cfg.websrv.userhalls_enabled !== false ? 'ENABLED' : 'DISABLED'}`); console.log(`[BOOT] Abyss: ${cfg.websrv.abyss_enabled !== false ? 'ENABLED' : 'DISABLED'}`); //console.timeEnd("loading"); // websrv const app = new flummpress(); const router = app.router; const tpl = app.tpl; // Security headers — applied to every response before any route logic app.use(async (req, res) => { const isSecure = cfg.main.url?.full?.startsWith('https'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); if (isSecure) { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } }); // Block source map requests early — flummpress crashes on unknown MIME types (e.g. .map) app.use(async (req, res) => { if (req.url?.pathname?.endsWith('.map')) { res.writeHead(404, { 'Content-Type': 'text/plain' }).end('Not Found'); } }); // Handle missing default avatar with a redirect to 404.gif app.use(async (req, res) => { if (req.url.pathname === '/a/default.png') { const defaultAvatar = path.join(cfg.paths.a, 'default.png'); if (!fs.existsSync(defaultAvatar)) { res.writeHead(302, { 'Location': '/s/img/404.gif' }).end(); req.url.pathname = '/default_avatar_redirect_bypass'; } } }); app.use(async (req, res) => { // This can be used to annoy people on discord sending links to your site lmao, shouldnt be used though since it sucks ass // if (cfg.main.development && req.method === 'POST') console.error(`[BOOT] [DEBUG_POST] ${req.method} ${req.url.pathname}`); // const ua = (req.headers['user-agent'] || '').toLowerCase(); // if (ua.includes('discordbot')) { // const isOembed = req.url.pathname.endsWith('.json'); // if (isOembed) { // res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ // type: "link", // version: "1.0", // title: "STOP USING DISCORD", // provider_name: "STOP USING DISCORD", // provider_url: "https://example.com" // })); // req.url.pathname = '/discordbot'; // return; // } // res.writeHead(200, { 'Content-Type': 'text/html' }).end(` // //
// // // // // // // // //
// //
//