diff --git a/.dockerignore b/.dockerignore index 6efa0a6..7c2790e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,3 +22,6 @@ tmp/ # Documentation README.md LICENSE + +postgres/ +f0ckm-data/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index c472e6b..6a34cab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +f0ckm-data/ node_modules/ logs/*.log config.json @@ -10,4 +11,6 @@ deleted/t tmp/* tools public/a -public/tag_cache \ No newline at end of file +public/tag_cache +.env +config.json \ No newline at end of file diff --git a/build.bash b/build.bash new file mode 100644 index 0000000..e4b9516 --- /dev/null +++ b/build.bash @@ -0,0 +1,102 @@ +#!/bin/bash + +# f0ckm build script - compatible version +# Targets: master, dev + +# Detect docker compose command +if docker compose version >/dev/null 2>&1; then + DOCKER_COMPOSE="docker compose" +elif docker-compose version >/dev/null 2>&1; then + DOCKER_COMPOSE="docker-compose" +else + echo "Error: Neither 'docker compose' nor 'docker-compose' found" + exit 1 +fi + +# Parse arguments +TARGET="master" +CACHE_FLAG="" +BRANCH_TARGET="" + +for arg in "$@"; do + case $arg in + master|dev|stg) + TARGET=$arg + ;; + --no-cache) + CACHE_FLAG="--no-cache" + ;; + *) + if [ -z "$BRANCH_TARGET" ]; then + BRANCH_TARGET=$arg + fi + ;; + esac +done + +# Check if we are in a git repo +if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then + echo "Error: not a git repository" + exit 1 +fi + +# Set Image name and branch defaults +IMAGE_NAME="f0ckm" + +if [ "$TARGET" == "master" ]; then + echo "--- PRODUCTION DEPLOYMENT (master) ---" + BRANCH_TARGET="master" +elif [ "$TARGET" == "stg" ]; then + echo "--- STAGING DEPLOYMENT ---" + BRANCH_TARGET="dev" +else + echo "--- LOCAL DEVELOPMENT (dev) ---" + BRANCH_TARGET="dev" +fi + +# Checkout branch +echo "Ensuring branch: ${BRANCH_TARGET}" +git checkout "${BRANCH_TARGET}" 2>/dev/null + +# Pull latest changes +echo "Pulling latest changes for branch: $(git rev-parse --abbrev-ref HEAD)" +git pull + +# Get git short revision +REV=$(git rev-parse --short HEAD) +TAG="f0ckm-${TARGET}-${REV}" + +echo "Building image: ${IMAGE_NAME}:${TAG} for environment: ${TARGET} (Cache: ${CACHE_FLAG:-enabled})" + +# Build the image +docker build $CACHE_FLAG --build-arg GIT_HASH=${TAG} -f Dockerfile -t ${IMAGE_NAME}:${TAG} . + +# Also tag it as latest +echo "Tagging as ${IMAGE_NAME}:latest" +docker tag ${IMAGE_NAME}:${TAG} ${IMAGE_NAME}:latest + +# Update .env +if grep -q "^F0CKM_TAG=" .env; then + sed -i "s/^F0CKM_TAG=.*/F0CKM_TAG=${TAG}/" .env +else + [ -s .env ] && [ -n "$(tail -c 1 .env)" ] && echo "" >> .env + echo "F0CKM_TAG=${TAG}" >> .env +fi + +if grep -q "^F0CKM_IMAGE=" .env; then + sed -i "s|^F0CKM_IMAGE=.*|F0CKM_IMAGE=${IMAGE_NAME}|" .env +else + [ -s .env ] && [ -n "$(tail -c 1 .env)" ] && echo "" >> .env + echo "F0CKM_IMAGE=${IMAGE_NAME}" >> .env +fi + +# Deployment +echo "Restarting services with $DOCKER_COMPOSE..." +$DOCKER_COMPOSE down +$DOCKER_COMPOSE up -d + +# Cleanup +echo "Cleaning up old docker resources..." +docker system prune -af + +echo "Successfully updated ${TARGET} with tag ${TAG}" diff --git a/config_example.json b/config_example.json index 68150ec..f5f071a 100644 --- a/config_example.json +++ b/config_example.json @@ -46,7 +46,9 @@ "enable_global_chat": true, "enable_danmaku": true, "private_messages": true, - "halls_enabled": true, + "halls_enabled": false, + "userhalls_enabled": false, + "abyss_enabled": false, "meme_creator": true, "web_url_upload": true, @@ -71,7 +73,7 @@ "enable_autoplay": false, "enable_swiping": true, "enable_profile_description": true, - "use_ententeich": true, + "user_alternative_infobox": false, "enable_swf": true, "swf_thumb": "/s/img/swf.png", diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..192f8ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,81 @@ +services: + f0ckm: + container_name: f0ckm + user: "${UID:-1000}:${GID:-1000}" + image: ${f0ckm_IMAGE:-f0ckm}:${f0ckm_TAG:-latest} + build: + context: . + dockerfile: Dockerfile + args: + - GIT_HASH=${GIT_HASH:-unknown} + networks: + - f0ckm-net + volumes: + - ./config.json:/opt/f0bm/config.json + - ./f0ckm-data/a/:/opt/f0bm/public/a/ + - ./f0ckm-data/b/:/opt/f0bm/public/b/ + - ./f0ckm-data/t/:/opt/f0bm/public/t/ + - ./f0ckm-data/deleted/:/opt/f0bm/deleted/ + - ./f0ckm-data/pending/:/opt/f0bm/pending/ + - ./f0ckm-data/emojis/:/opt/f0bm/public/s/emojis/ + - ./f0ckm-data/memes/:/opt/f0bm/public/memes/ + - ./f0ckm-data/ca/:/opt/f0bm/public/ca/ + - ./f0ckm-data/tmp/:/opt/f0bm/tmp/ + - ./f0ckm-data/logs/:/opt/f0bm/logs/ + - ./f0ckm-data/tag_cache/:/opt/f0bm/public/tag_cache/ + - ./f0ckm-data/fonts/:/opt/f0bm/public/s/fonts/ + - ./f0ckm-data/hall_cache/:/opt/f0bm/public/hall_cache/ + - ./f0ckm-data/hall_custom/:/opt/f0bm/public/hall_custom/ + - ./f0ckm-data/manifest.json:/opt/f0bm/public/manifest.json + + environment: + - GIT_HASH=${f0ckm_TAG:-unknown} + ports: + - "1337:1337" + restart: unless-stopped + depends_on: + f0ckm-db: + condition: service_healthy + + f0ckm-db: + container_name: f0ckm-db + image: postgres:17 + environment: + POSTGRES_DB: f0ckm + POSTGRES_USER: f0ckm + POSTGRES_PASSWORD: f0ckm + PGDATA: /data/postgres + volumes: + - ./postgres:/data/postgres + ports: + - "5454:5432" + networks: + - f0ckm-net + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 3s + timeout: 3s + retries: 5 + start_period: 5s + start_interval: 1s + + # anubis: + # image: ghcr.io/techarohq/anubis:latest + # container_name: anubis + # ports: + # - "3000:3000" + # environment: + # - BIND=:3000 + # - TARGET=http://f0ckm:1337 + # - DIFFICULTY=15 + # - POLICY_FNAME=/policy.yaml + # volumes: + # - ./botPolicy.yaml:/policy.yaml:ro + # networks: + # - f0ckm-net + # restart: unless-stopped + +networks: + f0ckm-net: + driver: bridge \ No newline at end of file diff --git a/migrations/f0ckm_schema.sql b/migrations/f0ckm_schema.sql index a64c8ef..2991028 100644 --- a/migrations/f0ckm_schema.sql +++ b/migrations/f0ckm_schema.sql @@ -77,7 +77,7 @@ CREATE FUNCTION public.fill_normalized() RETURNS trigger LANGUAGE plpgsql AS $$ begin - NEW.normalized = slugify(NEW.tag); + NEW.normalized = public.slugify(NEW.tag); return NEW; end$$; @@ -186,7 +186,7 @@ CREATE FUNCTION public.slugify(v text) RETURNS text LANGUAGE plpgsql AS $$ BEGIN - RETURN trim(BOTH '-' FROM regexp_replace(lower(unaccent(trim(v))), '[\u0000-\u002f \u003a-\u0040\u005b-\u0060\u007b-\u00bf]+', '', 'gi')); + RETURN trim(BOTH '-' FROM regexp_replace(lower(public.unaccent(trim(v))), '[\u0000-\u002f \u003a-\u0040\u005b-\u0060\u007b-\u00bf]+', '', 'gi')); END; $$; @@ -202,9 +202,9 @@ CREATE FUNCTION public.trg_update_xd_score() RETURNS trigger AS $$ BEGIN IF TG_OP = 'DELETE' THEN - PERFORM update_item_xd_score(OLD.item_id); + PERFORM public.update_item_xd_score(OLD.item_id); ELSE - PERFORM update_item_xd_score(NEW.item_id); + PERFORM public.update_item_xd_score(NEW.item_id); END IF; RETURN NULL; END; @@ -222,7 +222,7 @@ CREATE FUNCTION public.unaccent_text(text) RETURNS text AS $_$ -- unaccent is STABLE, but the indexes must use IMMUTABLE functions. -- comment this line out when calling pg_dump. - SELECT unaccent($1); + SELECT public.unaccent($1); -- Uncomment this line when calling pg_dump. --SELECT ''::text; @@ -1171,7 +1171,8 @@ CREATE TABLE public.user_options ( quote_emojis boolean DEFAULT true NOT NULL, embed_youtube_in_comments boolean DEFAULT true NOT NULL, hide_koepfe boolean DEFAULT false NOT NULL, - language text + language text, + use_alternative_infobox boolean DEFAULT false ); @@ -2426,3 +2427,46 @@ GRANT ALL ON SCHEMA public TO PUBLIC; \unrestrict ifoUZevi3oYdI7OmgFxUaco0kNV6kdlFS55QWa8PuaWXA3AY2nPUcs8ekmXvMEU + +-- +-- Data for Name: site_settings; Type: TABLE DATA; Schema: public; Owner: f0ckm +-- + +INSERT INTO public.site_settings (key, value) VALUES +('motd', 'Hello World!'), +('manual_approval', 'true'), +('min_tags', '1'), +('registration_open', 'false'), +('trusted_uploads', '0'), +('about_text', 'About'), +('rules_text', 'foobar'), +('terms_text', 'baz') +ON CONFLICT (key) DO NOTHING; + +-- +-- Data for Name: halls; Type: TABLE DATA; Schema: public; Owner: f0ckm +-- + +INSERT INTO public.halls (name, slug, rating) VALUES +('SFW', 'sfw', 'sfw'), +('NSFW', 'nsfw', 'nsfw') +ON CONFLICT (slug) DO NOTHING; + + +-- +-- Core Tags Data +-- + +INSERT INTO public.tags (id, tag, normalized) VALUES +(1, 'sfw', 'sfw'), +(2, 'nsfw', 'nsfw'), +(3, 'nsfp', 'nsfp'), +(4, 'nsfl', 'nsfl') +ON CONFLICT (id) DO UPDATE SET tag = EXCLUDED.tag, normalized = EXCLUDED.normalized; + +-- Ensure nsfp filter table has the correct IDs +INSERT INTO public.tags_nsfp (id) VALUES (3), (4) +ON CONFLICT DO NOTHING; + +-- Fix sequence +SELECT setval('public.tags_id_seq', (SELECT MAX(id) FROM public.tags)); diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index a81d88f..bc85117 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -1,14 +1,5 @@ -/* ============================================= - f0ckm.css - Unified Stylesheet - Merged: f0ck.css + w0bm.css + view styles - Born anew => f0ckm.css - ============================================= */ - -/* f0ckwork omega */ -/* written by sirx for f0ck.me */ -/* use whatever you like */ +/* f0ckwork beyond what was ever imagined, spiced up with 3928484 spices */ /* once upon a time this was a stiefelstrapse! but no more! */ -/* Licensed under wtfpl */ html[theme='f0ck'] { --accent: #9f0; @@ -1445,7 +1436,7 @@ body.sidebar-right-hidden .global-sidebar-right { z-index: 1; } - body.layout-legacy .item-layout-container .ententeich-block { + body.layout-legacy .item-layout-container .user-infobox-block { width: 800px; max-width: 100%; } @@ -10967,15 +10958,15 @@ textarea#profile_description { } /* ============================================= - ENTENTEICH PROFILE STYLES + USER ALTERNATIVE INFOBOX STYLES ============================================= */ -/* When ENTEN profile is active, expand .blahlol to full metadata width */ -.blahlol:has(.ententeich-block) { +/* When user infobox is active, expand .blahlol to full metadata width */ +.blahlol:has(.user-infobox-block) { grid-column: 1 / -1; } -.ententeich-block { +.user-infobox-block { display: flex; gap: 10px; background: rgba(0,0,0,0.2); @@ -10990,43 +10981,43 @@ textarea#profile_description { } -.enten-avatar { +.user-infobox-avatar { flex-shrink: 0; } -.enten-avatar img { +.user-infobox-avatar img { width: 64px; height: 64px; object-fit: cover; } -.enten-info { +.user-infobox-info { flex-grow: 1; display: flex; flex-direction: column; } -.enten-header { +.user-infobox-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } -.enten-username { +.user-infobox-username { font-weight: 700; color: var(--author-accent, inherit) !important; text-decoration: none; } -.enten-timestamp { +.user-infobox-timestamp { font-size: 0.8em; color: #888; letter-spacing: 0.5px; } -.enten-description { +.user-infobox-description { font-size: 0.9em; line-height: 1.5; color: #ccc; diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index 5c2f282..23a45f5 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -13,10 +13,10 @@ "mod": "mod", "settings": "Einstellungen", "logout": "Abmelden", - "notifications": "Benachrichtigungen", + "notifications": "Nuttis", "mark_all_read": "Alle als gelesen markieren", - "no_notifications": "Keine neuen Benachrichtigungen", - "view_all_notifications": "Alle anzeigen", + "no_notifications": "Keine neuen Nuttis", + "view_all_notifications": "Alle Nuttis anzeigen", "notif_tab_user": "Benutzer", "notif_tab_system": "System", "manage_subscriptions": "Abonnements verwalten", @@ -127,6 +127,8 @@ "show_motd": "Nachricht des Tages (MOTD) anzeigen", "modern_layout": "Modernes Layout", "modern_layout_hint": "3-Spalten-Layout", + "alternative_infobox": "Alternativer Autor-Infoblock", + "alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten", "disable_autoplay": "Automatische Wiedergabe deaktivieren", "disable_autoplay_hint": "Verhindert die automatische Wiedergabe von Videos und Audio", "disable_swiping": "Wischen deaktivieren", @@ -267,7 +269,7 @@ }, "comments": { "write_comment": "Kommentar schreiben...", - "post": "Senden", + "post": "Abschnalzen", "cancel": "Abbrechen" }, "upload_btn": { @@ -416,7 +418,7 @@ "loading": "Gespräche werden geladen…", "decrypting": "Nachrichten werden entschlüsselt…", "input_placeholder": "Nachricht schreiben…", - "send": "Senden" + "send": "Abschnalzen" }, "profile": { "message_btn": "Nachricht", diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index b7b430e..890986f 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -127,6 +127,8 @@ "show_motd": "Show Message of the Day (MOTD)", "modern_layout": "Modern layout", "modern_layout_hint": "3 Column Layout", + "alternative_infobox": "Alternative Author Infobox", + "alternative_infobox_hint": "Show a rich author card with avatar and bio on item pages", "disable_autoplay": "Disable Autoplay", "disable_autoplay_hint": "Prevent videos and audio from playing automatically", "disable_swiping": "Disable Swiping", diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index eed3719..3d12676 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -127,6 +127,8 @@ "show_motd": "Toon Bericht van de Dag (MOTD)", "modern_layout": "Moderne layout", "modern_layout_hint": "Indeling met 3 kolommen", + "alternative_infobox": "Alternatief auteur-informatievak", + "alternative_infobox_hint": "Toont een uitgebreide auteurkaart met avatar en bio op itempagina's", "disable_autoplay": "Automatisch afspelen uitschakelen", "disable_autoplay_hint": "Voorkomen dat video's en audio automatisch worden afgespeeld", "disable_swiping": "Swipen uitschakelen", diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index 5dbd7d3..546a06e 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -127,6 +127,8 @@ "show_motd": "Nachricht des Tages (NdT) anzeigen", "modern_layout": "Modernes Layout", "modern_layout_hint": "3-Spalten-Layout", + "alternative_infobox": "Alternativer Autor-Infoblock", + "alternative_infobox_hint": "Zeigt einen erweiterten Autor-Block mit Avatar und Bio auf Beitragsseiten", "disable_autoplay": "Automatische Wiedergabe deaktivieren", "disable_autoplay_hint": "Vermeiden Sie das automatische Abspielen von Videos und Tondateien", "disable_swiping": "Wischen deaktivieren", diff --git a/src/inc/routes/apiv2/settings.mjs b/src/inc/routes/apiv2/settings.mjs index dfeb4c8..37455f1 100644 --- a/src/inc/routes/apiv2/settings.mjs +++ b/src/inc/routes/apiv2/settings.mjs @@ -593,6 +593,23 @@ export default router => { } }); + // Update alternative infobox preference (per-user toggle for the rich author block) + group.put(/\/alternative_infobox/, lib.loggedin, async (req, res) => { + const use_alternative_infobox = req.post.use_alternative_infobox === true || req.post.use_alternative_infobox === 'true'; + try { + await db` + update user_options + set use_alternative_infobox = ${use_alternative_infobox} + where user_id = ${+req.session.id} + `; + if (req.session) req.session.use_alternative_infobox = use_alternative_infobox; + return res.json({ success: true, use_alternative_infobox }, 200); + } catch (e) { + console.error('Update alternative_infobox error:', e); + return res.json({ success: false, msg: 'Error updating preference' }, 500); + } + }); + // Update per-user language preference group.put(/\/language/, lib.loggedin, async (req, res) => { if (cfg.websrv.allow_language_change === false) { diff --git a/src/inc/routes/halls.mjs b/src/inc/routes/halls.mjs index a1e467f..0cfffd0 100644 --- a/src/inc/routes/halls.mjs +++ b/src/inc/routes/halls.mjs @@ -9,6 +9,7 @@ import fs from "fs/promises"; export default (router, tpl) => { // Main Halls Overview router.get(/^\/halls$/, async (req, res) => { + if (cfg.websrv.halls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); const mode = req.mode ?? 0; const excludedTags = req.session ? (req.session.excluded_tags || []) : []; diff --git a/src/inc/routes/index.mjs b/src/inc/routes/index.mjs index 9abb457..248cbab 100644 --- a/src/inc/routes/index.mjs +++ b/src/inc/routes/index.mjs @@ -202,6 +202,11 @@ export default (router, tpl) => { const tRouteStart = Date.now(); const mode = req.params.itemid ? 'item' : 'index'; + // Feature flag guards for disabled features + if (cfg.websrv.halls_enabled === false && req.params.hall) { + return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); + } + // Auto-persist strict mode from URL to session if it's there if (req.session && (req.query?.strict !== undefined || req.url.qs?.strict !== undefined)) { req.session.strict_mode = (req.query?.strict === '1' || req.url.qs?.strict === '1'); @@ -341,6 +346,8 @@ export default (router, tpl) => { data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : ''); data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : ''); data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : ''; + // Per-user alternative infobox preference overrides the site-wide config default + if (session) data.user_alternative_infobox = !!session.use_alternative_infobox; } res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); diff --git a/src/inc/routes/scroller.mjs b/src/inc/routes/scroller.mjs index ac2a45c..6611270 100644 --- a/src/inc/routes/scroller.mjs +++ b/src/inc/routes/scroller.mjs @@ -5,6 +5,7 @@ import lib from "../lib.mjs"; export default (router, tpl) => { // Serve the scroller page router.get(/^\/abyss\/?$/, async (req, res) => { + if (cfg.websrv.abyss_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); if (cfg.websrv.private_society && !req.session) { return res.reply({ code: 502, body: '502 Bad Gateway' }); } @@ -26,6 +27,7 @@ export default (router, tpl) => { // Lightweight meta refresh — returns live counts + tags for a batch of item IDs // GET /api/v2/scroller/meta?ids=1,2,3 router.get(/^\/api\/v2\/scroller\/meta\/?$/, async (req, res) => { + if (cfg.websrv.abyss_enabled === false) return res.reply({ code: 404, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: false }) }); if (cfg.websrv.private_society && !req.session) { return res.reply({ code: 502, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); } @@ -66,6 +68,7 @@ export default (router, tpl) => { // Tag autocomplete endpoint router.get(/^\/api\/v2\/scroller\/tags\/?$/, async (req, res) => { + if (cfg.websrv.abyss_enabled === false) return res.reply({ code: 404, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([]) }); if (cfg.websrv.private_society && !req.session) { return res.reply({ code: 502, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([]) }); } @@ -97,6 +100,11 @@ export default (router, tpl) => { // JSON API: returns a batch of items for the scroller router.get(/^\/api\/v2\/scroller\/feed\/?$/, async (req, res) => { + if (cfg.websrv.abyss_enabled === false) return res.reply({ + code: 404, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ success: false, items: [] }) + }); if (cfg.websrv.private_society && !req.session) { return res.reply({ code: 502, diff --git a/src/inc/routes/user_halls.mjs b/src/inc/routes/user_halls.mjs index 2061aa4..4ca9743 100644 --- a/src/inc/routes/user_halls.mjs +++ b/src/inc/routes/user_halls.mjs @@ -43,6 +43,7 @@ export default (router, tpl) => { // List halls for a user router.get(/^\/user\/(?[^/]+)\/halls\/?$/, async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); const ownerName = decodeURIComponent(req.params.owner); const mode = req.mode ?? 0; const excludedTags = req.session ? (req.session.excluded_tags || []) : []; @@ -79,6 +80,7 @@ export default (router, tpl) => { // Item grid for a user hall router.get(/^\/user\/(?[^/]+)\/hall\/(?[^/]+)(?:\/p\/(?\d+))?\/?$/, async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); const ownerName = decodeURIComponent(req.params.owner); const slug = decodeURIComponent(req.params.slug); @@ -124,6 +126,7 @@ export default (router, tpl) => { // Single item within a user hall router.get(/^\/user\/(?[^/]+)\/hall\/(?[^/]+)\/(?\d+)\/?$/, async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); const ownerName = decodeURIComponent(req.params.owner); const slug = decodeURIComponent(req.params.slug); @@ -249,6 +252,7 @@ export default (router, tpl) => { // ── API: list own halls (for modal) ──────────────────────────────────────── router.get(/^\/api\/v2\/me\/halls\/?$/, async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false })); if (!requireLogin(req, res)) return; try { const halls = await f0cklib.getUserHalls(req.session.id, 3, [], req.session.id); @@ -261,6 +265,7 @@ export default (router, tpl) => { // ── API: create hall ──────────────────────────────────────────────────────── router.post(/^\/api\/v2\/me\/halls\/?$/, async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false })); if (!requireLogin(req, res)) return; const name = (req.post.name || '').trim(); const slug = slugify(req.post.slug || name); @@ -279,6 +284,7 @@ export default (router, tpl) => { // ── API: update hall ──────────────────────────────────────────────────────── router.patch(/^\/api\/v2\/me\/halls\/(?[^/]+)\/?$/, async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false })); if (!requireLogin(req, res)) return; const slug = decodeURIComponent(req.params.slug); const { name, slug: newSlugRaw, description, is_private } = req.post; @@ -297,6 +303,7 @@ export default (router, tpl) => { // ── API: delete hall ──────────────────────────────────────────────────────── router.delete(/^\/api\/v2\/me\/halls\/(?[^/]+)\/?$/, async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return res.writeHead(404, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false })); if (!requireLogin(req, res)) return; const slug = decodeURIComponent(req.params.slug); diff --git a/src/index.mjs b/src/index.mjs index 1d15f3b..fc68416 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -134,8 +134,15 @@ process.on('uncaughtException', err => { self._trigger.set(trigger.name, new self.trigger(trigger)); }); - // Initial halls cache - await updateHallsCache(); + // 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"); @@ -227,7 +234,7 @@ process.on('uncaughtException', err => { if (req.cookies.session) { const user = await db` - select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language + select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, COALESCE("user_options".use_alternative_infobox, false) as use_alternative_infobox from "user_sessions" left join "user" on "user".id = "user_sessions".user_id left join "user_options" on "user_options".user_id = "user_sessions".user_id @@ -416,9 +423,9 @@ process.on('uncaughtException', err => { if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return; if (['/login', '/register', '/api/v2/upload', '/api/v2/settings/uploadAvatar', '/api/v2/admin/memes', '/api/v2/admin/emojis', '/api/v2/meta/extract-file', '/api/v2/meta/strip-gps', '/api/v2/scroller/external/rehost-meta'].includes(req.url.pathname)) return; // Hall manager routes are handled by bypass middleware with their own session auth - if (req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return; + if (cfg.websrv.halls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/admin\/halls(\/|$)/)) return; // User hall image upload is handled by bypass middleware below - if (req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return; + if (cfg.websrv.userhalls_enabled !== false && req.url.pathname.match(/^\/api\/v2\/me\/halls\/[^/]+\/image$/)) return; if (!validateCsrf(req, res)) return; }); @@ -484,6 +491,7 @@ process.on('uncaughtException', err => { // Bypass middleware for hall image uploads (multipart — needs raw body) app.use(async (req, res) => { + if (cfg.websrv.halls_enabled === false) return; const hallImgMatch = req.url.pathname.match(/^\/api\/v2\/admin\/halls\/([^/]+)\/image$/); if (hallImgMatch) { console.error('[BOOT] [HALL BYPASS] Image path hit:', req.method, req.url.pathname, 'cookies:', JSON.stringify(Object.keys(req.cookies || {}))); @@ -518,6 +526,7 @@ process.on('uncaughtException', err => { // Bypass middleware for user hall image uploads (multipart — raw body needed) app.use(async (req, res) => { + if (cfg.websrv.userhalls_enabled === false) return; const userHallImgMatch = req.url.pathname.match(/^\/api\/v2\/me\/halls\/([^/]+)\/image$/); if (userHallImgMatch && req.method === 'POST') { console.error('[BOOT] [USER_HALL BYPASS] Image upload:', req.url.pathname); @@ -701,6 +710,9 @@ process.on('uncaughtException', err => { get rules_text() { return getRulesText(); }, get terms_text() { return getTermsText(); }, get halls() { return getHalls(); }, + halls_enabled: cfg.websrv.halls_enabled !== false, + userhalls_enabled: cfg.websrv.userhalls_enabled !== false, + abyss_enabled: cfg.websrv.abyss_enabled !== false, smtp_enabled: !!(cfg.smtp && cfg.smtp.enabled && cfg.smtp.mail_reset_password), show_background_cfg: cfg.websrv.background !== false, allowed_mimes: Object.keys(cfg.mimes).concat([...new Set(Object.values(cfg.mimes))].map(ext => `.${ext}`)).join(','), @@ -727,7 +739,7 @@ process.on('uncaughtException', err => { get default_layout() { return getDefaultLayout(); }, show_koepfe: !!cfg.websrv.show_koepfe, allow_language_change: cfg.websrv.allow_language_change !== false, - use_ententeich: !!cfg.websrv.use_ententeich, + user_alternative_infobox: !!cfg.websrv.user_alternative_infobox, enable_xd_score: !!cfg.websrv.enable_xd_score, enable_swf: !!cfg.websrv.enable_swf, enable_danmaku: cfg.websrv.enable_danmaku !== false, diff --git a/views/item-partial-legacy.html b/views/item-partial-legacy.html index 40c6b8e..99a9874 100644 --- a/views/item-partial-legacy.html +++ b/views/item-partial-legacy.html @@ -64,10 +64,10 @@
- @if(use_ententeich) -
+ @if(user_alternative_infobox) +
-
+ -
-
-
- {!! item.author_display_name || item.username !!} +
+
+ - +
-
+
{!! item.author_description || '' !!}
@@ -94,16 +94,16 @@ - {{ item.id }} - @if(item.src.short)@if(!use_ententeich) — @endif{{ item.src.short }}@endif - @if(session && !use_ententeich) — [{!! item.author_display_name || item.username || 'unknown' !!}] @endif - @if(item.is_oc)@if(!use_ententeich || item.src.short) — @endifOC@endif + {{ item.id }} + @if(item.src.short)@if(!user_alternative_infobox) — @endif{{ item.src.short }}@endif + @if(session && !user_alternative_infobox) — [{!! item.author_display_name || item.username || 'unknown' !!}] @endif + @if(item.is_oc)@if(!user_alternative_infobox || item.src.short) — @endifOC@endif - @if(!use_ententeich) — — @endif - @if(item.primaryHall) + @if(!user_alternative_infobox) — — @endif + @if(halls_enabled && item.primaryHall) {{ item.primaryHall.name }}@if(is_mod_or_admin) @endif@if(item.otherHalls && item.otherHalls.length)+{{ item.otherHalls.length }}@each(item.otherHalls as oh){{ oh.name }}@endeach@endif - @if(!use_ententeich) —@endif + @if(!user_alternative_infobox) —@endif @endif @@ -117,7 +117,9 @@ @endif + @if(halls_enabled) + @endif @if(can_manage_item) @if(can_extract_meta) diff --git a/views/item-partial-modern.html b/views/item-partial-modern.html index f9a8dd4..c082a24 100644 --- a/views/item-partial-modern.html +++ b/views/item-partial-modern.html @@ -109,7 +109,7 @@ {{ item.id }}@if(item.src.short) — {{ item.src.short }}@endif @if(session) — [{!! item.author_display_name || item.username || 'unknown' !!}] @endif @if(item.is_oc) — OC@endif — - @if(item.primaryHall) + @if(halls_enabled && item.primaryHall) {{ item.primaryHall.name }}@if(is_mod_or_admin) @endif@if(item.otherHalls && item.otherHalls.length)+{{ item.otherHalls.length }}@each(item.otherHalls as oh){{ oh.name }}@endeach@endif — @@ -131,7 +131,9 @@ @endif @endif + @if(halls_enabled) + @endif @if(is_mod_or_admin) diff --git a/views/settings.html b/views/settings.html index 4c25981..64be610 100644 --- a/views/settings.html +++ b/views/settings.html @@ -76,6 +76,15 @@ {{ t('settings.modern_layout_hint') }}
+ @if(!session.use_new_layout) +
+ + {{ t('settings.alternative_infobox_hint') }} +
+ @endif
@endif - @if(enable_xd_score) -
- {{ t('settings.content_filters') }} -
- -
- - {{ session.min_xd_score || 0 }} - - - -
- {{ t('settings.min_xd_score_hint') }} -
-
-
- @endif +

{{ t('settings.account') }}