This commit is contained in:
2026-08-07 21:42:12 +02:00
parent c7414e4b21
commit feb408338a
40 changed files with 927 additions and 229 deletions

View File

@@ -38,6 +38,16 @@ export default new class {
.replace(/'/g, "'");
}
generateSlug(length = 11) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
let slug = '';
const bytes = crypto.randomBytes(length);
for (let i = 0; i < length; i++) {
slug += chars[bytes[i] % chars.length];
}
return slug;
}
formatSize(size, i = ~~(Math.log(size) / Math.log(1024))) {
return (size / Math.pow(1024, i)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][i];
};

View File

@@ -169,6 +169,8 @@
"favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.",
"hide_fav_badge": "Favoriten-Badge-Avatar verbergen",
"hide_fav_badge_hint": "Zeigt auf Beitragsseiten stattdessen ein Geist-Icon ohne Profilverlinkung an",
"default_upload_visibility": "Standard Upload-Sichtbarkeit",
"default_upload_visibility_hint": "Lege die Standard-Sichtbarkeit für deine neuen Uploads fest.",
"image_expand_on_click": "Bilder beim Klicken inline erweitern",
"image_expand_on_click_hint": "Anstatt das Scroll-Zoom-Modal zu öffnen, wird ein Bild beim Klicken innerhalb der Seite auf volle Größe erweitert.",
"enable_bg_blur": "Hintergrundunschärfe aktivieren",
@@ -799,6 +801,12 @@
"delete_confirm": "Diesen Einladungstoken löschen?",
"slot_refreshes_on": "Slot erneuert sich am {date}",
"slot_refreshed": "Slot erneuert",
"admin_desc": "Du bist Admin, leg los."
"admin_desc": "Du bist Admin, leg los.",
"visibility": {
"public": "Öffentlich",
"unlisted": "Nicht gelistet",
"private": "Privat",
"change_visibility": "Sichtbarkeit ändern"
}
}
}

View File

@@ -169,6 +169,8 @@
"favorites_private_hint": "Only you and administrators can view your favorites list.",
"hide_fav_badge": "Hide Favorite Badge Avatar",
"hide_fav_badge_hint": "Display as a ghost icon on post detail pages without linking to your profile",
"default_upload_visibility": "Default Upload Visibility",
"default_upload_visibility_hint": "Set the default visibility level for your new uploads.",
"image_expand_on_click": "Expand images inline on click",
"image_expand_on_click_hint": "Instead of opening the scroll zoom modal, clicking an image will expand it to full size within the page.",
"enable_bg_blur": "Enable Background blur",
@@ -801,6 +803,12 @@
"delete_confirm": "Delete this invite token?",
"slot_refreshes_on": "slot refreshes on {date}",
"slot_refreshed": "slot refreshed",
"admin_desc": "You are an admin, go ahead."
"admin_desc": "You are an admin, go ahead.",
"visibility": {
"public": "Public",
"unlisted": "Unlisted",
"private": "Private",
"change_visibility": "Change Visibility"
}
}
}

View File

@@ -302,6 +302,9 @@ export default {
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
}
const isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
const visibilityFilter = isOwnerOrAdmin ? db`` : db`and coalesce(items.visibility, 0) = 0`;
const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger });
let total = getCachedCount(cacheKey);
@@ -313,6 +316,7 @@ export default {
where
${db.unsafe(modequery)}
and items.active = true
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and fav_u.user ilike ${user}` : db``}
@@ -355,6 +359,7 @@ export default {
where
${db.unsafe(modequery)}
and items.active = true
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and fav_u.user ilike ${user}` : db``}
@@ -385,6 +390,8 @@ export default {
const rows = (await db`
select
items.id,
items.slug,
items.visibility,
items.mime,
items.dest,
items.username as username,
@@ -521,7 +528,17 @@ export default {
if (uhData.length) userHallObj = uhData[0];
}
const mime = (rawMime ?? "");
const itemid = rawItemid ? +rawItemid : null;
const rawIdOrSlug = rawItemid ?? null;
if (rawIdOrSlug === null || rawIdOrSlug === undefined || rawIdOrSlug === '') {
return {
success: false,
message: "404 - upload not found"
};
}
const isNumeric = /^\d+$/.test(String(rawIdOrSlug));
const itemLookup = isNumeric ? db`items.id = ${+rawIdOrSlug}` : db`items.slug = ${String(rawIdOrSlug)}`;
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
const mimeSQL = mimeParts.length > 0
? db`and (${mimeParts.map(m => m === 'flash'
@@ -535,19 +552,12 @@ export default {
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
const isStrict = strictParams.length > 0;
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall, mime, itemid, strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall, mime, itemid: rawIdOrSlug, strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
const effMode = Number(mode ?? 0);
const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
const modequery = multiRatingSQL ?? lib.getMode(effMode);
if (itemid === null) {
return {
success: false,
message: "404 - upload not found"
};
}
let tagFilter = db``;
let titleFilter = db``;
if (isTitleSearch && titleQuery) {
@@ -588,6 +598,7 @@ export default {
return db`
${db.unsafe(modequery)}
and items.active = true
and coalesce(items.visibility, 0) = 0
${tagFilter}
${titleFilter}
${hallFilter}
@@ -601,11 +612,9 @@ export default {
};
const startTime = Date.now();
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Starting fetch for itemid=${itemid}`);
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Starting fetch for rawIdOrSlug=${rawIdOrSlug}`);
// 1. Fetch the main item
// We only apply the active check and global NSFW filter (for guests) here.
// We skip the 'mode' preference filter so that switching modes on an item view doesn't result in a 404 (post not visible).
const items = await db`
select distinct on (items.id)
items.*,
@@ -629,7 +638,7 @@ export default {
left join "user_options" uo on uo.user_id = author_u.id
${user_id ? db`left join user_video_views uvv on uvv.video_id = items.id and uvv.user_id = ${user_id}` : db``}
where
items.id = ${itemid} and
${itemLookup} and
items.active = true
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
limit 1
@@ -637,7 +646,41 @@ export default {
const actitem = items[0];
if (actitem && user_id) {
if (!actitem) {
return {
success: false,
message: "404 - upload not found"
};
}
const itemid = actitem.id;
// Check visibility permissions:
const isOwnerOrAdmin = session && (
(session.user && session.user.toLowerCase() === (actitem.username || '').toLowerCase()) ||
session.admin || session.is_moderator
);
// If request was by sequential numeric ID (/123) and item visibility > 0 (unlisted/private):
// Block numeric enumeration unless viewer is owner/admin
if (isNumeric && actitem.visibility > 0 && !isOwnerOrAdmin) {
return {
success: false,
message: "404 - upload not found"
};
}
// If item is Private (visibility === 2):
// Direct link only allowed for owner/admin
if (actitem.visibility === 2 && !isOwnerOrAdmin) {
return {
success: false,
is_private: true,
message: "403 - private upload"
};
}
if (user_id) {
db`
insert into user_video_views (user_id, video_id, view_count, last_viewed)
values (${user_id}, ${itemid}, 1, now())
@@ -646,34 +689,26 @@ export default {
last_viewed = now()
`.catch(e => console.error('Failed to track view:', e));
}
if (!actitem) {
// Item not found or filtered out - check if it exists but was filtered (for OG meta tags)
if (!session && getGlobalfilter()) {
// Guest global filter check if item was filtered out
if (!session && getGlobalfilter() && !actitem) {
const unfilteredItem = await db`
select id from items where id = ${itemid} and active = true limit 1
select id from items where ${itemLookup} and active = true limit 1
`;
if (unfilteredItem[0]) {
// Item exists but was filtered - return minimal data for OG tags with blurred thumbnail
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
return {
success: false,
message: "Sorry, this post is currently not visible.",
item: {
id: itemid,
og_thumbnail: `${cfg.websrv.paths.thumbnails}/${itemid}_blur.webp`,
id: unfilteredItem[0].id,
og_thumbnail: `${cfg.websrv.paths.thumbnails}/${unfilteredItem[0].id}_blur.webp`,
og_url: hallSlug
? `https://${cfg.main.url.domain}/h/${encodeURIComponent(hallSlug)}/${itemid}`
: `https://${cfg.main.url.domain}/${itemid}`,
? `https://${cfg.main.url.domain}/h/${encodeURIComponent(hallSlug)}/${unfilteredItem[0].id}`
: `https://${cfg.main.url.domain}/${unfilteredItem[0].id}`,
og_description: `Content not visible in current mode`
}
};
}
}
return {
success: false,
message: "Sorry, this post is currently not visible."
};
}
// 2. Fetch Next/Prev/Start/End/Cheat in parallel
@@ -685,7 +720,7 @@ export default {
const baseQuery = (whereClause, orderBy, limit = 1) => {
return db`
select items.id
select items.id, items.slug
from items
left join tags_assign on tags_assign.item_id = items.id
left join tags on tags.id = tags_assign.tag_id
@@ -696,7 +731,7 @@ export default {
where
${buildConditions()}
${whereClause}
group by items.id
group by items.id, items.slug
${orderBy}
limit ${limit}
`;
@@ -711,7 +746,7 @@ export default {
const checkFilter = !session && nsfpIds.length > 0;
const query = db`
SELECT ta.item_id as id
SELECT ta.item_id as id, items.slug
FROM tags_assign ta
INNER JOIN items ON items.id = ta.item_id
${checkFilter
@@ -781,24 +816,24 @@ export default {
if (actitem.checksum && actitem.checksum.includes('_bypass_')) {
const baseChecksum = actitem.checksum.split('_bypass_')[0];
const repostRows = await db`
SELECT id, username, stamp FROM items
SELECT id, slug, username, stamp FROM items
WHERE active = true
AND id != ${itemid}
AND (checksum = ${baseChecksum} OR checksum LIKE ${baseChecksum + '_bypass_%'})
ORDER BY id ASC
`;
repostItems = repostRows.map(r => ({ id: r.id, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
repostItems = repostRows.map(r => ({ id: r.id, slug: r.slug, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
} else if (actitem.checksum) {
// Even without bypass, check if other bypass-entries exist with this same hash
const baseChecksum = actitem.checksum;
const repostRows = await db`
SELECT id, username, stamp FROM items
SELECT id, slug, username, stamp FROM items
WHERE active = true
AND id != ${itemid}
AND checksum LIKE ${baseChecksum + '_bypass_%'}
ORDER BY id ASC
`;
repostItems = repostRows.map(r => ({ id: r.id, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
repostItems = repostRows.map(r => ({ id: r.id, slug: r.slug, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
}
// Also find visually-similar items via phash, merging with checksum results
@@ -808,7 +843,7 @@ export default {
const existingIds = new Set(repostItems.map(r => r.id));
for (const pm of phashMatches) {
if (!existingIds.has(pm.id)) {
repostItems.push({ id: pm.id, username: pm.username, stamp: pm.stamp, match_type: 'phash' });
repostItems.push({ id: pm.id, slug: pm.slug, username: pm.username, stamp: pm.stamp, match_type: 'phash' });
existingIds.add(pm.id);
}
}
@@ -843,7 +878,7 @@ export default {
else if (userMode === 4 && (!cfg.enable_nsfl || !isNsfl)) modeBlocked = true; // NSFL mode, item is not NSFL
else if (userMode === 2 && isTagged) modeBlocked = true; // Untagged mode, item has tags
if (modeBlocked) {
if (modeBlocked && !isOwnerOrAdmin && actitem.visibility !== 1) {
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
return {
success: false,
@@ -871,6 +906,8 @@ export default {
},
item: {
id: actitem.id,
slug: actitem.slug || null,
visibility: actitem.visibility !== undefined ? actitem.visibility : 0,
username: actitem.username,
author_id: actitem.author_id,
author_color: actitem.author_color,
@@ -933,13 +970,13 @@ export default {
height: actitem.height || null,
original_filename: actitem.original_filename || null
},
title: `${actitem.id} - ${cfg.websrv.domain}`,
title: `${(cfg.enable_item_slugs !== false && actitem.slug) ? actitem.slug : actitem.id} - ${cfg.websrv.domain}`,
pagination: {
end: endItem[0]?.id || itemid,
start: startItem[0]?.id || itemid,
next: nextItem[0]?.id || null,
prev: prevItem[0]?.id || null,
page: actitem.id,
end: (cfg.enable_item_slugs !== false && endItem[0]?.slug) ? endItem[0].slug : (endItem[0]?.id || itemid),
start: (cfg.enable_item_slugs !== false && startItem[0]?.slug) ? startItem[0].slug : (startItem[0]?.id || itemid),
next: (cfg.enable_item_slugs !== false && nextItem[0]?.slug) ? nextItem[0].slug : (nextItem[0]?.id || null),
prev: (cfg.enable_item_slugs !== false && prevItem[0]?.slug) ? prevItem[0].slug : (prevItem[0]?.id || null),
page: (cfg.enable_item_slugs !== false && actitem.slug) ? actitem.slug : actitem.id,
cheat: cheat
},
phrase: cfg.websrv.phrases[~~(Math.random() * cfg.websrv.phrases.length)],

View File

@@ -4,7 +4,7 @@ import cfg from "../config.mjs";
import { createI18n } from "../i18n.mjs";
export default (router, tpl) => {
router.get(/\/ajax\/item\/(?<itemid>\d+)/, async (req, res) => {
router.get(/\/ajax\/item\/(?<itemid>[a-zA-Z0-9_-]{11}|\d+)/, async (req, res) => {
const tAjaxStart = Date.now();
let query = {};
if (typeof req.url === 'string') {
@@ -35,12 +35,12 @@ export default (router, tpl) => {
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/(\d+)/)?.[1];
const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/([a-zA-Z0-9_-]{11}|\d+)/)?.[1];
const data = await f0cklib.getf0ck({
itemid: itemid,
mode: query.mode !== undefined ? +query.mode : req.mode,
ratings: ratingsArr,
session: !!req.session,
session: req.session,
url: contextUrl,
user: query.user,
tag: query.tag,
@@ -170,7 +170,8 @@ export default (router, tpl) => {
html: itemHtml,
pagination: paginationHtml,
title: data.title,
id: itemid
id: itemid,
slug: data.item?.slug || null
})
});
});

View File

@@ -594,10 +594,12 @@ export default router => {
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
strict: isStrict,
session: !!req.session,
exclude: req.session?.excluded_tags || []
exclude: req.session?.excluded_tags || [],
user_id: req.session?.id,
is_admin: req.session?.admin
});
if (!data.itemid) {
if (!data || !data.itemid) {
return res.json({
success: false,
items: []
@@ -636,6 +638,7 @@ export default router => {
items: {
...safeItem,
id: item.id,
slug: item.slug || null,
dest: relativeDest,
url: directUrl,
direct_url: directUrl
@@ -1038,7 +1041,24 @@ export default router => {
});
group.post(/\/togglefav$/, lib.loggedin, async (req, res) => {
const postid = +req.post.postid;
const rawPostid = req.post?.postid ?? req.body?.postid ?? req.url?.qs?.postid;
if (rawPostid === undefined || rawPostid === null) {
return res.json({ success: false, msg: 'Missing postid' }, 400);
}
// Support both numeric item ID and string slug
const isNumeric = /^\d+$/.test(String(rawPostid));
const itemRow = await db`
SELECT id FROM items
WHERE ${isNumeric ? db`id = ${+rawPostid}` : db`slug = ${String(rawPostid)}`} AND active = true AND is_deleted = false
LIMIT 1
`;
if (!itemRow.length) {
return res.json({ success: false, msg: 'Item not found' }, 404);
}
const postid = itemRow[0].id;
// Check if already faved by this user — compare as numbers to avoid type mismatch
const existing = await db`
@@ -1168,6 +1188,47 @@ export default router => {
});
});
group.post(/\/item\/visibility$/, lib.loggedin, async (req, res) => {
if (cfg.enable_private_uploads === false) {
return res.json({ success: false, msg: 'Private uploads feature disabled' }, 403);
}
const postid = req.post?.postid || req.post?.id || req.body?.postid || req.body?.id;
const visibility = parseInt(req.post?.visibility ?? req.body?.visibility, 10);
if (!postid || isNaN(visibility) || ![0, 1, 2].includes(visibility)) {
return res.json({ success: false, msg: 'Invalid parameters' }, 400);
}
const isNumeric = /^\d+$/.test(String(postid));
const item = await db`
SELECT id, slug, username, visibility
FROM items
WHERE ${isNumeric ? db`id = ${+postid}` : db`slug = ${String(postid)}`} AND active = true AND is_deleted = false
LIMIT 1
`;
if (item.length === 0) {
return res.json({ success: false, msg: 'Item not found' }, 404);
}
const isOwner = item[0].username === req.session.user;
const isAdmin = req.session.admin || req.session.is_moderator;
if (!isOwner && !isAdmin) {
return res.json({ success: false, msg: 'Unauthorized' }, 403);
}
await db`UPDATE items SET visibility = ${visibility} WHERE id = ${item[0].id}`;
f0cklib.clearCountCache();
return res.json({
success: true,
itemid: item[0].id,
slug: item[0].slug,
visibility: visibility
});
});
group.post(/\/item\/(?<id>[0-9]+)\/rating$/, lib.loggedin, async (req, res) => {
const itemid = +req.params.id;
if (!itemid) return res.json({ success: false, msg: 'No itemid provided' }, 400);

View File

@@ -353,6 +353,27 @@ export default router => {
}
});
// Update Default Upload Visibility preference
group.put(/\/default_upload_visibility/, lib.loggedin, async (req, res) => {
const vis = parseInt(req.post.default_upload_visibility, 10);
if (isNaN(vis) || ![0, 1, 2].includes(vis)) {
return res.json({ success: false, msg: 'Invalid visibility option' }, 400);
}
try {
await db`
update user_options
set default_upload_visibility = ${vis}
where user_id = ${+req.session.id}
`;
if (req.session) req.session.default_upload_visibility = vis;
return res.json({ success: true, default_upload_visibility: vis }, 200);
} catch (e) {
console.error('Update Default Upload Visibility pref error:', e);
return res.json({ success: false, msg: 'Error updating preference' }, 500);
}
});
// Update Username Color preference
group.put(/\/username_color/, lib.loggedin, async (req, res) => {
const { color } = req.post;

View File

@@ -137,6 +137,16 @@ const parseMultipart = (buffer, boundary) => {
import { getManualApproval, getMinTags, getBypassDuplicateCheck } from "../../settings.mjs";
const getTargetVisibility = (req, postVis) => {
if (cfg.enable_private_uploads === false) return 0;
const rawHeader = req.headers ? req.headers['x-upload-visibility'] : null;
const val = (rawHeader || postVis || '').toString().trim().toLowerCase();
if (val === 'private' || val === '2') return 2;
if (val === 'unlisted' || val === '1') return 1;
if (val === 'public' || val === '0') return 0;
return req.session?.default_upload_visibility || 0;
};
// Collect request body as buffer with debug logging
const collectBody = (req) => {
return new Promise((resolve, reject) => {
@@ -369,6 +379,9 @@ export default router => {
// Store as a YouTube embed: dest = yt:VIDEO_ID, mime = video/youtube
const filename = `yt:${videoId}`;
const targetVisibility = getTargetVisibility(req, req.post?.visibility);
const itemSlug = (cfg.enable_item_slugs !== false) ? lib.generateSlug(11) : null;
const [{ id: itemid }] = await db`
insert into items ${db({
src: ytUrl,
@@ -383,8 +396,10 @@ export default router => {
stamp: ~~(Date.now() / 1000),
active: !isApprovalRequired,
is_oc: !!is_oc,
title: title
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title')}
title: title,
visibility: targetVisibility,
slug: itemSlug
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title', 'visibility', 'slug')}
RETURNING id
`;
@@ -437,6 +452,9 @@ export default router => {
});
} else {
// ===== REGULAR URL DOWNLOAD (Asynchronous) =====
const targetVisibility = getTargetVisibility(req, req.post?.visibility);
const itemSlug = (cfg.enable_item_slugs !== false) ? lib.generateSlug(11) : null;
const session = {
id: req.session.id,
user: req.session.user,
@@ -689,8 +707,10 @@ export default router => {
stamp: ~~(Date.now() / 1000),
active: !isApprovalRequired,
is_oc: !!is_oc,
title: title
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title')}
title: title,
visibility: targetVisibility,
slug: itemSlug
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title', 'visibility', 'slug')}
RETURNING id
`;

View File

@@ -174,7 +174,7 @@ export default (router, tpl) => {
const modequery = (multiRatingSQL ?? lib.getMode(mode)).replace(/items\.id/g, 'i.id');
const comments = await db`
SELECT c.*, i.mime, i.id as item_id
SELECT c.*, i.mime, i.id as item_id, i.slug as item_slug
FROM comments c
LEFT JOIN items i ON c.item_id = i.id
WHERE c.user_id = ${userId} AND c.is_deleted = false
@@ -570,6 +570,7 @@ export default (router, tpl) => {
// Fetch the trigger-updated xd_score and the item rating tag from the DB (trigger runs synchronously before we get here)
const itemQuery = await db`
SELECT
i.slug,
i.xd_score,
(SELECT ta.tag_id FROM tags_assign ta
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])
@@ -592,6 +593,7 @@ export default (router, tpl) => {
type: 'comment',
id: commentId,
item_id: item_id,
item_slug: itemQuery[0]?.slug || null,
parent_id: parent_id || null,
body: notifyBody,
username: req.session.user,
@@ -1003,6 +1005,7 @@ export default (router, tpl) => {
c.*,
i.mime,
i.id as item_id,
i.slug as item_slug,
i.dest as item_dest,
(SELECT ta.tag_id FROM tags_assign ta
WHERE ta.item_id = i.id AND ta.tag_id = ANY(${[1, 2, cfg.nsfl_tag_id || 3]}::int[])

View File

@@ -73,7 +73,7 @@ export default (router, tpl) => {
ratings: ratingsArr,
mime: mime,
fav: false,
session: !!req.session,
session: req.session,
user_id: req.session?.id,
random: isRandom
});
@@ -253,7 +253,7 @@ export default (router, tpl) => {
console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`);
if (!data.success) {
if (data.is_private) {
if (data.is_private && (data.message === 'private favorites' || req.params.mode === 'favs')) {
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
return res.reply({
code: 403,
@@ -414,11 +414,11 @@ export default (router, tpl) => {
// Specific route for direct item links: /user/:user/:itemid
// This avoids ambiguity with the profile route
router.get(/\/user\/(?<user>[^/]+)\/(?<itemid>\d+)$/, handleGenericRoute);
router.get(/\/user\/(?<user>[^/]+)\/(?<itemid>[a-zA-Z0-9_-]+)$/, handleGenericRoute);
// Generic router for everything else (Index, Tags, standard User Grids)
// We exclude static paths (/s/, /b/, /t/, /ca/, /a/) to prevent the greedy regex from intercepting them.
router.get(/^(?!\/(s|b|t|ca|a)\/)\/?(?:\/tag\/(?<tag>.+?))?(?:\/h\/(?<hall>.+?))?(?:\/user\/(?<user>.+?)\/(?<mode>f0cks|uploads|favs))?(?:\/(?<mime>(?:video|audio|image)(?:,(?:video|audio|image))*))?(?:\/p\/(?<page>\d+))?(?:\/(?<itemid>\d+))?\/?(?:\?.*)?$/, handleGenericRoute);
// We exclude static paths (/s/, /b/, /t/, /ca/, /a/, system routes) to prevent the greedy regex from intercepting them.
router.get(/^(?!\/(s|b|t|ca|a|login|register|settings|about|terms|rules|api|logout|auth|admin|comments|notifications|feed)\/)\/?(?:\/tag\/(?<tag>.+?))?(?:\/h\/(?<hall>.+?))?(?:\/user\/(?<user>.+?)\/(?<mode>f0cks|uploads|favs))?(?:\/(?<mime>(?:video|audio|image)(?:,(?:video|audio|image))*))?(?:\/p\/(?<page>\d+))?(?:\/(?<itemid>[a-zA-Z0-9_-]{11}|\d+))?\/?(?:\?.*)?$/, handleGenericRoute);
/* </routing-refactor> */
router.get(/^\/(about)$/, (req, res) => {

View File

@@ -122,7 +122,7 @@ db.listen('activity', async (payload) => {
// We need the username, avatar, and item mime for the preview
// trigger only gave us user_id and item_id
const [details] = await db`
SELECT u.id as user_id, u.user as username, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name, i.mime,
SELECT u.id as user_id, u.user as username, uo.avatar, uo.avatar_file, uo.username_color, uo.display_name, i.mime, i.slug as item_slug,
(SELECT tag_id FROM tags_assign WHERE item_id = i.id AND tag_id IN (1, 2) LIMIT 1) as tag_id
FROM "user" u
LEFT JOIN user_options uo ON u.id = uo.user_id
@@ -138,6 +138,7 @@ db.listen('activity', async (payload) => {
data.username_color = details.username_color;
data.display_name = details.display_name || null;
data.tag_id = details.tag_id;
data.item_slug = details.item_slug;
} else {
data.username = 'System';
}
@@ -367,7 +368,7 @@ export default (router, tpl) => {
const typeFilter = tab === 'system' ? SYSTEM_TYPES : (tab === 'user' ? USER_TYPES : null);
const notifications = typeFilter
? await db`
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read, n.data,
SELECT n.id, n.type, n.item_id, i.slug as item_slug, n.reference_id, n.created_at, n.is_read, n.data,
COALESCE(u.user, 'System') as from_user,
COALESCE(uo.display_name, '') as from_display_name,
COALESCE(u.id, 0) as from_user_id,
@@ -392,7 +393,7 @@ export default (router, tpl) => {
OFFSET ${offset}
`
: await db`
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read, n.data,
SELECT n.id, n.type, n.item_id, i.slug as item_slug, n.reference_id, n.created_at, n.is_read, n.data,
COALESCE(u.user, 'System') as from_user,
COALESCE(uo.display_name, '') as from_display_name,
COALESCE(u.id, 0) as from_user_id,
@@ -443,7 +444,7 @@ export default (router, tpl) => {
try {
const notifications = await db`
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read, n.data,
SELECT n.id, n.type, n.item_id, i.slug as item_slug, n.reference_id, n.created_at, n.is_read, n.data,
COALESCE(u.user, 'System') as from_user,
COALESCE(uo.display_name, '') as from_display_name,
COALESCE(u.id, 0) as from_user_id,

View File

@@ -97,11 +97,11 @@ export default (router, tpl) => {
`;
const favotop = await db`
select favorites.item_id, count(*) favs
select favorites.item_id as id, items.slug, count(*) favs
from favorites
join items on items.id = favorites.item_id
where items.active = true
group by favorites.item_id
group by favorites.item_id, items.slug
having count(*) > 1
order by favs desc
limit 10
@@ -110,7 +110,7 @@ export default (router, tpl) => {
let xdtop = [];
if (config.websrv.enable_xd_score) {
const xdRows = await db`
select id, xd_score
select id, slug, xd_score
from items
where active = true and is_deleted = false and xd_score > 0
order by xd_score desc

View File

@@ -34,7 +34,7 @@ export default (router, tpl) => {
const subs = await db`
SELECT
s.created_at as sub_date,
i.id, i.dest, i.mime, i.username as uploader_name
i.id, i.slug, i.dest, i.mime, i.username as uploader_name
FROM comment_subscriptions s
JOIN items i ON s.item_id = i.id
WHERE s.user_id = ${req.session.id} AND s.is_subscribed = true
@@ -45,6 +45,7 @@ export default (router, tpl) => {
const items = subs.map(i => ({
id: i.id,
slug: i.slug || null,
user: i.uploader_name || 'System',
sub_created: new Date(i.sub_date).toLocaleString(),
thumb: `/t/${i.id}.webp`
@@ -108,7 +109,7 @@ export default (router, tpl) => {
const subs = await db`
SELECT
s.created_at as sub_date,
i.id, i.dest, i.mime, i.username as uploader_name
i.id, i.slug, i.dest, i.mime, i.username as uploader_name
FROM comment_subscriptions s
JOIN items i ON s.item_id = i.id
WHERE s.user_id = ${req.session.id} AND s.is_subscribed = true
@@ -118,6 +119,7 @@ export default (router, tpl) => {
const items = subs.map(i => ({
id: i.id,
slug: i.slug || null,
user: i.uploader_name || 'System',
sub_created: new Date(i.sub_date).toLocaleString(),
thumb: `/t/${i.id}.webp`

View File

@@ -92,7 +92,7 @@ export default (router, tpl) => {
const data = await f0cklib.getf0cks({
page: req.params.page,
mode: req.mode,
session: !!req.session,
session: req.session,
exclude: req.session?.excluded_tags || [],
user_id: req.session?.id,
userHall: slug,
@@ -138,7 +138,7 @@ export default (router, tpl) => {
const data = await f0cklib.getf0ck({
itemid: req.params.itemid,
mode: req.mode,
session: !!req.session,
session: req.session,
exclude: req.session?.excluded_tags || [],
user_id: req.session?.id,
userHall: slug,