getting things ready for release

This commit is contained in:
2026-04-27 05:29:26 +02:00
parent cdaf469a6d
commit 7815072d1e
21 changed files with 369 additions and 77 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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 || []) : [];

View File

@@ -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');

View File

@@ -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: '<html><body>502 Bad Gateway</body></html>' });
}
@@ -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,

View File

@@ -43,6 +43,7 @@ export default (router, tpl) => {
// List halls for a user
router.get(/^\/user\/(?<owner>[^/]+)\/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\/(?<owner>[^/]+)\/hall\/(?<slug>[^/]+)(?:\/p\/(?<page>\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\/(?<owner>[^/]+)\/hall\/(?<slug>[^/]+)\/(?<itemid>\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\/(?<slug>[^/]+)\/?$/, 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\/(?<slug>[^/]+)\/?$/, 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);

View File

@@ -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,