This commit is contained in:
2026-09-14 22:30:09 +02:00
parent 22ffc3b51a
commit 912deeb28e
42 changed files with 2998 additions and 618 deletions
+371 -107
View File
@@ -8,9 +8,30 @@ import fs from "fs";
import path from "path";
import url from "url";
const getGlobalfilter = () => {
const getGlobalfilter = (session) => {
if (!cfg.nsfp?.length) return null;
const filteredTags = cfg.websrv.public_nsfw ? cfg.nsfp.filter(id => id !== 2) : cfg.nsfp;
let filteredTags = [...cfg.nsfp];
// For anonymous users, respect their allowed_modes permissions
if (session && isAnonSession(session)) {
const allowedModes = getAnonAllowedModes();
if (allowedModes.includes('nsfw')) {
filteredTags = filteredTags.filter(id => id !== 2);
}
const nsflId = parseInt(cfg.nsfl_tag_id, 10) || 3;
if (allowedModes.includes('nsfl')) {
filteredTags = filteredTags.filter(id => id !== nsflId);
}
} else if (!session) {
// Guest (not logged in): use public_nsfw setting
if (cfg.websrv.public_nsfw) {
filteredTags = filteredTags.filter(id => id !== 2);
}
} else {
// Logged-in member: use public_nsfw setting
if (cfg.websrv.public_nsfw) {
filteredTags = filteredTags.filter(id => id !== 2);
}
}
return filteredTags.length ? filteredTags.map(n => `tag_id = ${n}`).join(" or ") : null;
};
@@ -175,7 +196,7 @@ function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, ex
hall ?? '',
mime ?? '',
fav ? 1 : 0,
session ? 1 : 0, // guests get globalfilter applied; members don't
(session && !isAnonSession(session)) ? 1 : 0, // guests and anon users get globalfilter applied; members don't
excludedTags.slice().sort().join(','),
newerThan ?? '',
minXd,
@@ -575,7 +596,7 @@ const f0cklib = {
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
@@ -601,7 +622,7 @@ const f0cklib = {
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
@@ -685,6 +706,28 @@ const f0cklib = {
visibilityFilter
} = filters;
const effMode = Number(mode ?? 0);
const nsflId = parseInt(cfg.nsfl_tag_id, 10) || 3;
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
let safeRatingsArr = ratingsArr;
const isGuest = !session || !session.user;
if (isGuest) {
safeRatingsArr = ['sfw'];
} else if (isAnonSession(session)) {
const allowedModes = getAnonAllowedModes();
const canFilter = canAnonDo('filter');
if (!canFilter) {
safeRatingsArr = null;
} else if (safeRatingsArr) {
safeRatingsArr = safeRatingsArr.filter(r => allowedModes.includes(r));
if (safeRatingsArr.length === 0) {
safeRatingsArr = (allowedModes.length === 1 && !allowedModes.includes('all'))
? [allowedModes[0]]
: (allowedModes.length < 5 ? [...allowedModes] : null);
}
}
}
const actPage = +(page ?? 1);
const eps = limit ?? cfg.websrv.eps;
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner, tagger };
@@ -709,7 +752,7 @@ const f0cklib = {
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
@@ -753,7 +796,7 @@ const f0cklib = {
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
@@ -821,12 +864,11 @@ const f0cklib = {
row.is_audio = !!(row.mime && row.mime.startsWith('audio/'));
}
if (tag && !isTitleSearch && rows.some(r => r.is_album)) {
if (rows.some(r => r.is_album)) {
const albumIds = rows.filter(r => r.is_album).map(r => r.id);
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0 && albumIds.length > 0) {
if (albumIds.length > 0) {
try {
const matchingSubs = await db`
const subItemsRows = await db`
SELECT
ai.item_id,
ai.id as subf0ck_id,
@@ -835,55 +877,135 @@ const f0cklib = {
ai.mime as subf0ck_mime,
ai.order_index
FROM album_items ai
JOIN album_items_tags_assign aita ON aita.album_item_id = ai.id
JOIN tags t ON t.id = aita.tag_id
${tagger ? db`JOIN "user" u ON u.id = aita.user_id` : db``}
WHERE ai.item_id = ANY(${albumIds}::int[])
${tagger ? db`AND u.user ILIKE ${tagger}` : db``}
AND (${isStrict
? db`t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))`
: db`${terms.map(term => db`t.normalized LIKE '%' || slugify(${term}) || '%'`).reduce((a, b) => db`${a} OR ${b}`)}`
})
ORDER BY ai.item_id, ai.order_index ASC
`;
const matchMap = new Map();
for (const ms of matchingSubs) {
if (!matchMap.has(ms.item_id)) {
matchMap.set(ms.item_id, {
first: ms,
matchingIds: new Set([ms.subf0ck_id])
});
} else {
matchMap.get(ms.item_id).matchingIds.add(ms.subf0ck_id);
}
const subIds = subItemsRows.map(s => s.subf0ck_id);
const subTagsRows = subIds.length > 0 ? await db`
SELECT aita.album_item_id, t.id, t.tag, t.normalized
FROM album_items_tags_assign aita
JOIN tags t ON t.id = aita.tag_id
WHERE aita.album_item_id = ANY(${subIds}::int[])
`.catch(() => []) : [];
const tagsBySubId = new Map();
for (const st of subTagsRows) {
if (!tagsBySubId.has(st.album_item_id)) tagsBySubId.set(st.album_item_id, []);
tagsBySubId.get(st.album_item_id).push(st);
}
const subsByItemId = new Map();
for (const si of subItemsRows) {
if (!subsByItemId.has(si.item_id)) subsByItemId.set(si.item_id, []);
subsByItemId.get(si.item_id).push(si);
}
for (const row of rows) {
if (matchMap.has(row.id)) {
const info = matchMap.get(row.id);
const ms = info.first;
row.target_subf0ck_slug = ms.subf0ck_slug || ms.subf0ck_id;
if (ms.subf0ck_dest) {
const subBase = ms.subf0ck_dest.replace(/\.[^.]+$/, '');
row.thumb = `/t/${subBase}.webp`;
row.matching_sub_thumb = `/t/${subBase}.webp`;
row.matching_sub_dest = ms.subf0ck_dest;
row.dest = ms.subf0ck_dest;
if (!row.is_album || !subsByItemId.has(row.id)) continue;
const subList = subsByItemId.get(row.id);
const isOwnerOrAdmin = (session && session.user && row.username && session.user.toLowerCase() === row.username.toLowerCase()) || (session && (session.admin || session.is_moderator));
const isSubVisible = (subTags) => {
if (isOwnerOrAdmin) return true;
const subIsNsfl = cfg.enable_nsfl && subTags.some(t => t.id == nsflId || t.normalized === 'nsfl');
const subIsNsfw = subTags.some(t => t.id == 2);
const subIsSfw = subTags.some(t => t.id == 1);
const subIsUntagged = !subIsSfw && !subIsNsfw && !subIsNsfl;
if (excludedTags && excludedTags.length > 0 && subTags.length > 0) {
if (subTags.some(t => excludedTags.includes(t.normalized) || excludedTags.includes(lib.slugify(t.tag)))) {
return false;
}
}
if (ms.subf0ck_mime) {
row.matching_sub_mime = ms.subf0ck_mime;
row.mime = ms.subf0ck_mime;
if (!session || !session.user) {
if (subIsNsfl) return false;
if (subIsNsfw && !cfg.websrv.public_nsfw) return false;
if (subIsUntagged && !cfg.websrv.public_untagged) return false;
if (effMode === 0 && (subIsNsfw || subIsNsfl || (subIsUntagged && !cfg.websrv.public_untagged))) return false;
if (effMode === 1 && !subIsNsfw) return false;
if (effMode === 2 && (!subIsUntagged || !cfg.websrv.public_untagged)) return false;
return true;
}
const mCount = info.matchingIds.size;
row.album_count = mCount;
if (mCount <= 1) {
row.is_album = false;
if (isAnonSession(session)) {
const allowedModes = getAnonAllowedModes();
if (subIsNsfl && !allowedModes.includes('nsfl')) return false;
if (subIsNsfw && !allowedModes.includes('nsfw')) return false;
if (subIsUntagged && !allowedModes.includes('untagged')) return false;
if (subIsSfw && !allowedModes.includes('sfw')) return false;
if (safeRatingsArr && safeRatingsArr.length > 0) {
const subRatingName = subIsNsfl ? 'nsfl' : (subIsNsfw ? 'nsfw' : (subIsSfw ? 'sfw' : 'untagged'));
if (!safeRatingsArr.includes(subRatingName)) return false;
} else if (effMode !== 3) {
if (effMode === 0 && (subIsNsfw || subIsNsfl || (subIsUntagged && !allowedModes.includes('untagged')))) return false;
if (effMode === 1 && !subIsNsfw) return false;
if (effMode === 2 && (!subIsUntagged || !allowedModes.includes('untagged'))) return false;
if (effMode === 4 && !subIsNsfl) return false;
}
return true;
}
if (safeRatingsArr && safeRatingsArr.length > 0) {
const subRatingName = subIsNsfl ? 'nsfl' : (subIsNsfw ? 'nsfw' : (subIsSfw ? 'sfw' : 'untagged'));
if (!safeRatingsArr.includes(subRatingName)) return false;
} else if (effMode !== 3) {
if (effMode === 0 && (subIsNsfw || subIsNsfl)) return false;
if (effMode === 1 && !subIsNsfw) return false;
if (effMode === 2 && !subIsUntagged) return false;
if (effMode === 4 && !subIsNsfl) return false;
}
return true;
};
let visibleSubs = subList.filter((si) => {
let st = tagsBySubId.get(si.subf0ck_id) || [];
return isSubVisible(st);
});
if (tag && !isTitleSearch) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
const tagMatchingSubs = visibleSubs.filter((si) => {
let st = tagsBySubId.get(si.subf0ck_id) || [];
if (isStrict) {
return strictParams.every(sp => st.some(t => t.normalized === sp));
}
return terms.some(term => {
const slugTerm = lib.slugify(term);
return st.some(t => t.normalized && t.normalized.includes(slugTerm));
});
});
if (tagMatchingSubs.length > 0) {
const ms = tagMatchingSubs[0];
row.target_subf0ck_slug = ms.subf0ck_slug || ms.subf0ck_id;
if (ms.subf0ck_dest) {
const subBase = ms.subf0ck_dest.replace(/\.[^.]+$/, '');
row.thumb = `/t/${subBase}.webp`;
row.matching_sub_thumb = `/t/${subBase}.webp`;
row.matching_sub_dest = ms.subf0ck_dest;
row.dest = ms.subf0ck_dest;
}
if (ms.subf0ck_mime) {
row.matching_sub_mime = ms.subf0ck_mime;
row.mime = ms.subf0ck_mime;
}
visibleSubs = tagMatchingSubs;
}
}
}
row.album_count = visibleSubs.length;
if (visibleSubs.length <= 1) {
row.is_album = false;
}
}
} catch (err) {
console.warn('[FEED] Error resolving matching subf0cks for tag search:', err.message);
console.warn('[FEED] Error calculating visible album count for feed:', err.message);
}
}
}
@@ -950,7 +1072,7 @@ const f0cklib = {
view_mode: fav ? 'favs' : 'uploads'
};
},
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang, ids, subf0ck } = {}) => {
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang, ids, subf0ck, bypass_filter } = {}) => {
if (fav && rawUser) {
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
if (isPrivate && !isAllowed) {
@@ -1029,6 +1151,21 @@ const f0cklib = {
const effMode = Number(mode ?? 0);
const nsflId = parseInt(cfg.nsfl_tag_id, 10) || 3;
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
let safeRatingsArr = ratingsArr;
if (!session || !session.user) {
safeRatingsArr = ['sfw'];
} else if (isAnonSession(session)) {
const allowedModes = getAnonAllowedModes();
if (safeRatingsArr) {
safeRatingsArr = safeRatingsArr.filter(r => allowedModes.includes(r));
if (safeRatingsArr.length === 0) {
safeRatingsArr = (allowedModes.length === 1 && !allowedModes.includes('all'))
? [allowedModes[0]]
: (allowedModes.length < 5 ? [...allowedModes] : null);
}
}
}
const itemModeQuery = computeBaseMode(mode, ratings, session);
let tagFilter = db``;
@@ -1089,7 +1226,7 @@ const f0cklib = {
${!fav && user ? db`and items.username ilike ${user}` : db``}
${idsFilter}
${mimeSQL}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
`;
};
@@ -1180,16 +1317,17 @@ const f0cklib = {
last_viewed = now()
`.catch(e => console.error('Failed to track view:', e));
}
// Guest rating restriction check for public items (unlisted items requested by direct link/slug bypass guest rating blocks)
if (!session && (actitem.visibility || 0) === 0) {
// Public visitor / guest rating restriction check for public items (unlisted items requested by direct link/slug bypass guest rating blocks)
const isPublicVisitor = !session || !session.user || isAnonSession(session);
if (isPublicVisitor && !isOwnerOrAdmin && (actitem.visibility || 0) === 0) {
let blocked = false;
if (getGlobalfilter()) {
if (getGlobalfilter(session)) {
const filteredItem = await db`
select 1 from tags_assign where item_id = ${itemid} and (${db.unsafe(getGlobalfilter())}) limit 1
select 1 from tags_assign where item_id = ${itemid} and (${db.unsafe(getGlobalfilter(session))}) limit 1
`;
if (filteredItem.length > 0) blocked = true;
}
if (!blocked && !cfg.websrv.public_untagged) {
if (!blocked && !session && !cfg.websrv.public_untagged) {
const ratingTag = await db`
select 1 from tags_assign where item_id = ${itemid} and tag_id in (1, 2, ${nsflId}) limit 1
`;
@@ -1217,7 +1355,7 @@ const f0cklib = {
// Determine the effective mode for optimization check (similar to Random)
const nsfl_id = cfg.nsfl_tag_id || 3;
const useTagsDriver = !!session && (effMode === 1 || effMode === 4) && !fav && !tag && !user && !hall && (!cleanIds || cleanIds.length === 0);
const useTagsDriver = !!session && !isAnonSession(session) && (effMode === 1 || effMode === 4) && !fav && !tag && !user && !hall && (!cleanIds || cleanIds.length === 0);
const baseQuery = (whereClause, orderBy, limit = 1) => {
return db`
@@ -1244,7 +1382,7 @@ const f0cklib = {
const useTagIdOpt = !mimeParts.includes('audio');
const nsfpIds = cfg.nsfp || [];
const checkFilter = !session && nsfpIds.length > 0;
const checkFilter = (!session || isAnonSession(session)) && nsfpIds.length > 0;
const query = db`
SELECT ta.item_id as id, items.slug
@@ -1305,13 +1443,27 @@ const f0cklib = {
link.suffix = '';
}
const favorites = await db`
select "user".user, "user_options".avatar, "user_options".avatar_file, "user_options".username_color, "user_options".display_name, "user_options".hide_fav_badge
select "favorites".user_id, "user".user, "user".login, "user_options".avatar, "user_options".avatar_file, "user_options".username_color, "user_options".display_name, "user_options".hide_fav_badge, "anon_identities".fingerprint as anon_fingerprint
from "favorites"
left join "user" on "user".id = "favorites".user_id
left join "user_options" on "user_options".user_id = "favorites".user_id
left join "anon_identities" on "anon_identities".user_id = "favorites".user_id
where "favorites".item_id = ${itemid}
`;
for (const f of favorites) {
if (f.anon_fingerprint || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_'))) {
f.is_anon = true;
f.user = 'anonymous';
f.login = 'anonymous';
f.display_name = 'Anonymous';
f.avatar = null;
f.avatar_file = null;
f.username_color = null;
f.hide_fav_badge = true;
}
}
// Detect reposts: items uploaded with bypass_duplicate_check have checksum = `{hash}_bypass_{ts}`
// Find all items (including this one) that share the same base checksum.
let repostItems = [];
@@ -1355,27 +1507,21 @@ const f0cklib = {
}
}
// Efficient coverart fallback with on-demand extraction
let hasCoverart = actitem.has_coverart;
if (!hasCoverart && actitem.mime?.startsWith('audio/')) {
const caPath = path.join(cfg.paths.ca, `${actitem.id}.webp`);
let hasCoverart = false;
if (actitem.mime && actitem.mime.startsWith('audio/')) {
try {
const caPath = path.join(cfg.paths.ca, `${actitem.id}.webp`);
if (fs.existsSync(caPath) && fs.statSync(caPath).size > 0) {
hasCoverart = true;
db`UPDATE items SET has_coverart = TRUE WHERE id = ${actitem.id}`.catch(() => {});
} else {
// Attempt extraction directly from audio file if embedded
const sourcePath = path.join(cfg.paths.b, actitem.dest);
if (fs.existsSync(sourcePath)) {
await queue.spawn('ffmpeg', ['-y', '-i', sourcePath, '-an', '-vcodec', 'webp', '-frames:v', '1', caPath], { quiet: true }).catch(() => {});
// On-demand cover art extraction fallback
const audioFile = path.join(cfg.paths.b, actitem.dest);
if (fs.existsSync(audioFile)) {
await queue.spawn('ffmpeg', ['-y', '-i', audioFile, '-an', '-vcodec', 'webp', '-frames:v', '1', caPath], { quiet: true });
if (fs.existsSync(caPath) && fs.statSync(caPath).size > 0) {
hasCoverart = true;
db`UPDATE items SET has_coverart = TRUE WHERE id = ${actitem.id}`.catch(() => {});
const tPath = path.join(cfg.paths.t, `${actitem.id}.webp`);
if (!fs.existsSync(tPath) || fs.statSync(tPath).size === 0) {
await queue.spawn('magick', [caPath + '[0]', '-resize', '256x256^', '-gravity', 'center', '-crop', '256x256+0+0', '+repage', tPath], { quiet: true }).catch(() => {});
}
await queue.spawn('magick', [caPath + '[0]', '-resize', '256x256^', '-gravity', 'center', '-crop', '256x256+0+0', '+repage', tPath], { quiet: true });
} else {
try { fs.unlinkSync(caPath); } catch (_) {}
}
@@ -1416,11 +1562,8 @@ const f0cklib = {
if (tag && !isTitleSearch) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0 && tagsBySubId.size > 0) {
const matching = albumRows.filter((r, idx) => {
const matching = albumRows.filter((r) => {
let st = tagsBySubId.get(r.id) || [];
if (st.length === 0 && (r.order_index === 0 || idx === 0)) {
st = tags;
}
if (isStrict) {
return strictParams.every(sp => st.some(t => t.normalized === sp));
}
@@ -1435,6 +1578,88 @@ const f0cklib = {
}
}
const isSubVisible = (subTags) => {
if (bypass_filter) return true;
const subIsNsfl = cfg.enable_nsfl && subTags.some(t => t.id == nsfl_id || t.normalized === 'nsfl');
const subIsNsfw = subTags.some(t => t.id == 2);
const subIsSfw = subTags.some(t => t.id == 1);
const subIsUntagged = !subIsSfw && !subIsNsfw && !subIsNsfl;
// Excluded tags check
if (excludedTags && excludedTags.length > 0 && subTags.length > 0) {
if (subTags.some(t => excludedTags.includes(t.normalized) || excludedTags.includes(lib.slugify(t.tag)))) {
return false;
}
}
// Guest check
if (!session || !session.user) {
if (subIsNsfl) return false;
if (subIsNsfw && !cfg.websrv.public_nsfw) return false;
if (subIsUntagged && !cfg.websrv.public_untagged) return false;
if (effMode === 0 && (subIsNsfw || subIsNsfl || (subIsUntagged && !cfg.websrv.public_untagged))) return false;
if (effMode === 1 && !subIsNsfw) return false;
if (effMode === 2 && (!subIsUntagged || !cfg.websrv.public_untagged)) return false;
return true;
}
// Anon session check
if (isAnonSession(session)) {
const allowedModes = getAnonAllowedModes();
if (subIsNsfl && !allowedModes.includes('nsfl')) return false;
if (subIsNsfw && !allowedModes.includes('nsfw')) return false;
if (subIsUntagged && !allowedModes.includes('untagged')) return false;
if (subIsSfw && !allowedModes.includes('sfw')) return false;
if (safeRatingsArr && safeRatingsArr.length > 0) {
const subRatingName = subIsNsfl ? 'nsfl' : (subIsNsfw ? 'nsfw' : (subIsSfw ? 'sfw' : 'untagged'));
if (!safeRatingsArr.includes(subRatingName)) return false;
} else if (effMode !== 3) {
if (effMode === 0 && (subIsNsfw || subIsNsfl || (subIsUntagged && !allowedModes.includes('untagged')))) return false;
if (effMode === 1 && !subIsNsfw) return false;
if (effMode === 2 && (!subIsUntagged || !allowedModes.includes('untagged'))) return false;
if (effMode === 4 && !subIsNsfl) return false;
}
return true;
}
// Logged-in member check
if (safeRatingsArr && safeRatingsArr.length > 0) {
const subRatingName = subIsNsfl ? 'nsfl' : (subIsNsfw ? 'nsfw' : (subIsSfw ? 'sfw' : 'untagged'));
if (!safeRatingsArr.includes(subRatingName)) return false;
} else if (effMode !== 3) {
if (effMode === 0 && (subIsNsfw || subIsNsfl)) return false;
if (effMode === 1 && !subIsNsfw) return false;
if (effMode === 2 && !subIsUntagged) return false;
if (effMode === 4 && !subIsNsfl) return false;
}
return true;
};
albumRows = albumRows.filter((r) => {
let subTags = tagsBySubId.get(r.id) || [];
return isSubVisible(subTags);
});
if (albumRows.length === 0) {
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
return {
success: false,
message: "Sorry, this post is currently not visible.",
item: {
id: itemid,
slug: actitem.slug || null,
og_thumbnail: `${cfg.websrv.paths.thumbnails}/${itemid}_blur.webp`,
og_url: hallSlug
? `https://${cfg.main.url.domain}/h/${encodeURIComponent(hallSlug)}/${itemid}`
: `https://${cfg.main.url.domain}/${itemid}`,
og_description: `Content not visible in current mode`
}
};
}
album = await Promise.all(albumRows.map(async (r, idx) => {
const order = idx;
const subSlug = r.slug || r.id;
@@ -1503,9 +1728,6 @@ const f0cklib = {
}
let subTags = tagsBySubId.get(r.id) || [];
if (subTags.length === 0 && (r.order_index === 0 || idx === 0)) {
subTags = tags;
}
return {
id: r.id,
@@ -1537,6 +1759,21 @@ const f0cklib = {
const found = album.findIndex(s => String(s.slug || '') === String(requestedSubf0ckSlug) || String(s.subf0ck_id || '') === String(requestedSubf0ckSlug) || String(s.id) === String(requestedSubf0ckSlug));
if (found !== -1) {
reqIdx = found;
} else {
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
return {
success: false,
message: "Sorry, this post is currently not visible.",
item: {
id: itemid,
slug: actitem.slug || null,
og_thumbnail: `${cfg.websrv.paths.thumbnails}/${itemid}_blur.webp`,
og_url: hallSlug
? `https://${cfg.main.url.domain}/h/${encodeURIComponent(hallSlug)}/${itemid}`
: `https://${cfg.main.url.domain}/${itemid}`,
og_description: `Content not visible in current mode`
}
};
}
}
@@ -1560,12 +1797,10 @@ const f0cklib = {
}
if (targetSub.tags && targetSub.tags.length > 0) {
effectiveItemTags = targetSub.tags;
if (!effectiveItemTags.some(t => t.id === 1 || t.id === 2 || t.normalized === 'nsfl')) {
const ratingTag = tags.find(t => t.id === 1 || t.id === 2 || t.normalized === 'nsfl');
if (ratingTag) {
effectiveItemTags = [ratingTag, ...effectiveItemTags];
}
}
} else if (reqIdx === 0) {
effectiveItemTags = tags;
} else {
effectiveItemTags = [];
}
}
}
@@ -1577,7 +1812,7 @@ const f0cklib = {
const duration = Date.now() - startTime;
console.log(`[${new Date().toISOString()}] [GETF0CK_OPT] Fetch complete in ${duration}ms`);
const isNsfl = cfg.enable_nsfl && effectiveItemTags.some(t => t.id == nsfl_id);
const isNsfl = cfg.enable_nsfl && effectiveItemTags.some(t => t.id == nsfl_id || t.normalized === 'nsfl');
const isNsfw = effectiveItemTags.some(t => t.id == 2);
const isSfw = effectiveItemTags.some(t => t.id == 1);
const isTagged = effectiveItemTags.length > 0;
@@ -1604,20 +1839,15 @@ const f0cklib = {
}
};
}
}
} else if (isAnonSession(session) && !isOwnerOrAdmin && (actitem.visibility || 0) === 0) {
const allowedModes = getAnonAllowedModes();
let anonBlocked = false;
if (isNsfw && !allowedModes.includes('nsfw')) anonBlocked = true;
else if (isNsfl && !allowedModes.includes('nsfl')) anonBlocked = true;
else if (isUntagged && !allowedModes.includes('untagged')) anonBlocked = true;
else if (isSfw && !allowedModes.includes('sfw')) anonBlocked = true;
// Mode-mismatch visibility check:
// Only enforce for members (session users) with an explicit mode preference.
// Mode 0=sfw, 1=nsfw, 2=untagged, 3=all
const userMode = Number(mode ?? 0);
if (userMode !== 3) {
let modeBlocked = false;
if (userMode === 0 && (isNsfw || isNsfl || (!session && isUntagged && !cfg.websrv.public_untagged))) modeBlocked = true; // SFW mode, item is NSFW or NSFL
else if (userMode === 1 && !isNsfw) modeBlocked = true; // NSFW mode, item is not NSFW
else if (userMode === 4 && (!cfg.enable_nsfl || !isNsfl)) modeBlocked = true; // NSFL mode, item is not NSFL
else if (userMode === 2 && (isTagged || (!session && !cfg.websrv.public_untagged))) modeBlocked = true; // Untagged mode, item has tags
if (modeBlocked && !isOwnerOrAdmin && actitem.visibility !== 1) {
if (anonBlocked) {
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
return {
success: false,
@@ -1634,6 +1864,40 @@ const f0cklib = {
}
}
// Mode-mismatch visibility check:
// Mode 0=sfw, 1=nsfw, 2=untagged, 3=all
const userMode = Number(mode ?? 0);
if (!bypass_filter) {
let modeBlocked = false;
const untaggedDisallowed = (!session && !cfg.websrv.public_untagged) || (isAnonSession(session) && !getAnonAllowedModes().includes('untagged'));
if (safeRatingsArr && safeRatingsArr.length > 0) {
const itemRatingName = isNsfl ? 'nsfl' : (isNsfw ? 'nsfw' : (isSfw ? 'sfw' : 'untagged'));
if (!safeRatingsArr.includes(itemRatingName)) modeBlocked = true;
} else if (userMode !== 3) {
if (userMode === 0 && (isNsfw || isNsfl || (isUntagged && untaggedDisallowed))) modeBlocked = true; // SFW mode, item is NSFW or NSFL
else if (userMode === 1 && !isNsfw) modeBlocked = true; // NSFW mode, item is not NSFW
else if (userMode === 4 && (!cfg.enable_nsfl || !isNsfl)) modeBlocked = true; // NSFL mode, item is not NSFL
else if (userMode === 2 && (isTagged || untaggedDisallowed)) modeBlocked = true; // Untagged mode, item has tags
}
if (modeBlocked) {
const hallSlug = hall && typeof hall === 'object' ? hall.slug : hall;
return {
success: false,
message: "Sorry, this post is currently not visible.",
item: {
id: itemid,
slug: actitem.slug || null,
og_thumbnail: `${cfg.websrv.paths.thumbnails}/${itemid}${isNsfw ? '_blur' : ''}.webp`,
og_url: hallSlug
? `https://${cfg.main.url.domain}/h/${encodeURIComponent(hallSlug)}/${itemid}`
: `https://${cfg.main.url.domain}/${itemid}`,
og_description: `Content not visible in current mode`
}
};
}
}
if (getEnableItemSlugs() && !actitem.slug) {
actitem.slug = lib.generateSlug(11);
db`UPDATE items SET slug = ${actitem.slug} WHERE id = ${actitem.id} AND (slug IS NULL OR slug = '')`.catch(e => console.error('[AUTO_SLUG] Failed DB update:', e.message));
@@ -1796,7 +2060,7 @@ const f0cklib = {
AND items.title ILIKE ${'%' + titleQuery + '%'}
AND items.title IS NOT NULL
${mimeSQL}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
ORDER BY random()
LIMIT 1
@@ -1817,7 +2081,7 @@ const f0cklib = {
and items.active = true
and coalesce(items.visibility, 0) = 0
${mimeSQL}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
group by items.id
order by random()
limit 1
@@ -1860,7 +2124,7 @@ const f0cklib = {
${user ? db`and items.username ilike ${user}` : db``}
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
${mimeSQL}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
group by items.id, tags.tag
order by random()
@@ -1880,7 +2144,7 @@ const f0cklib = {
and items.active = true
and coalesce(items.visibility, 0) = 0
${mimeSQL}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
order by random()
limit 1
@@ -1906,13 +2170,13 @@ const f0cklib = {
// When multi-rating SQL is active, use it directly. Otherwise use the tag-join optimisation.
const globalModeQuery = modequery;
// tagId optimisation only applies for single native modes for logged-in users (not multi-rating or guest mode)
const tagId = session && !multiRatingSQL && (mode === 0 || mode === 1 || mode === 4)
const tagId = session && !isAnonSession(session) && !multiRatingSQL && (mode === 0 || mode === 1 || mode === 4)
? (mode === 4 ? (cfg.nsfl_tag_id || 3) : (mode === 1 ? 2 : 1))
: null;
// If audio is included, we avoid the strict tagId optimization to ensure audio is visible
const useTagIdOpt = tagId && !mimeParts.includes('audio');
const nsfpIds = cfg.nsfp || [];
const checkFilter = !session && nsfpIds.length > 0;
const checkFilter = (!session || isAnonSession(session)) && nsfpIds.length > 0;
// Use a single uniform query with ORDER BY random()
// For 30k-100k items, this is performant enough and much more reliable than seeking.
@@ -2452,7 +2716,7 @@ const f0cklib = {
getRandomRecommendations: async ({ limit = 20, mode, ratings, session, exclude, user_id, is_admin, mime, exclude_ids } = {}) => {
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
const modequery = computeBaseMode(mode, ratingsArr, session);
const globalfilter = !session ? getGlobalfilter() : null;
const globalfilter = (!session || isAnonSession(session)) ? getGlobalfilter(session) : null;
const excludedTags = session && exclude ? (exclude || []) : [];
const maxLimit = Math.min(Math.max(1, Number(limit) || 20), 50);
@@ -2741,7 +3005,7 @@ const f0cklib = {
} = {}) => {
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
const modequery = computeBaseMode(mode, ratingsArr, session);
const globalfilter = !session ? getGlobalfilter() : null;
const globalfilter = (!session || isAnonSession(session)) ? getGlobalfilter(session) : null;
const excludedTags = session && exclude ? (exclude || []) : [];
const maxLimit = Math.min(Math.max(1, Number(limit) || 20), 50);
@@ -3059,7 +3323,7 @@ const f0cklib = {
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
const modequery = computeBaseMode(mode, ratingsArr, session);
const globalfilter = !session ? getGlobalfilter() : null;
const globalfilter = (!session || isAnonSession(session)) ? getGlobalfilter(session) : null;
const excludedTags = session && exclude ? (exclude || []) : [];
const maxLimit = Math.min(Math.max(1, Number(limit) || 20), 50);
const numOffset = Math.max(0, Number(offset) || 0);