This commit is contained in:
2026-09-18 00:32:53 +02:00
parent c8cfe07c70
commit 74f7884525
19 changed files with 1996 additions and 483 deletions
+63 -38
View File
@@ -11,26 +11,26 @@ import url from "url";
const getGlobalfilter = (session) => {
if (!cfg.nsfp?.length) return null;
let filteredTags = [...cfg.nsfp];
// For anonymous users, respect their allowed_modes permissions
// allowed_modes controls access to NSFW/NSFL for anon/guests by stripping the RATING tags
// (tag_id 2 = NSFW, nsfl_tag_id = NSFL) from the nsfp filter.
// Any other nsfp tags (content-specific blocks) are never stripped — they always apply.
const nsflId = parseInt(cfg.nsfl_tag_id, 10) || 3;
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
// Guest (not logged in): use public_nsfw / public_nsfl settings
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);
}
// Logged-in member: rating tags are not filtered by nsfp
filteredTags = filteredTags.filter(id => id !== 2 && id !== nsflId);
}
return filteredTags.length ? filteredTags.map(n => `tag_id = ${n}`).join(" or ") : null;
};
@@ -363,7 +363,8 @@ const buildFeedFilters = async ({
rawUserHallOwner,
rawTagger
}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const user = rawUser ? decodeURI(rawUser) : null;
const userLike = user ? lib.escapeLike(user) : null;
// --- title: prefix — search items.title instead of the tags table ---
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
@@ -485,6 +486,7 @@ const buildFeedFilters = async ({
return {
user,
userLike,
_decodedTag,
isTitleSearch,
titleQuery,
@@ -568,6 +570,7 @@ const f0cklib = {
const {
user,
userLike,
mimeSQL,
excludedTags,
newerThan,
@@ -591,8 +594,8 @@ const f0cklib = {
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${fav ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
@@ -617,8 +620,8 @@ const f0cklib = {
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${fav ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
@@ -680,6 +683,7 @@ const f0cklib = {
const {
user,
userLike,
_decodedTag,
isTitleSearch,
titleQuery,
@@ -747,8 +751,8 @@ const f0cklib = {
${tagFilter}
${titleFilter}
${idsFilter}
${fav && user ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${fav && user ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
@@ -791,8 +795,8 @@ const f0cklib = {
${tagFilter}
${titleFilter}
${idsFilter}
${fav && user ? db`and (fav_u.user ilike ${user} or fav_u.login ilike ${user})` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${fav && user ? db`and (fav_u.user ilike ${userLike} or fav_u.login ilike ${userLike})` : db``}
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
@@ -1089,7 +1093,8 @@ const f0cklib = {
: (typeof ids === 'string' ? ids.split(',').map(Number).filter(n => Number.isInteger(n) && n > 0) : null);
const idsFilter = (cleanIds && cleanIds.length > 0) ? db`and items.id = ANY(${cleanIds}::int[])` : db``;
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const user = rawUser ? decodeURI(rawUser) : null;
const userLike = user ? lib.escapeLike(user) : null;
// --- title: prefix — search items.title instead of the tags table ---
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
@@ -1222,8 +1227,8 @@ const f0cklib = {
${titleFilter}
${hallFilter}
${userHallFilter}
${fav && user ? db`and "user"."user" ilike ${user}` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${fav && user ? db`and "user"."user" ilike ${userLike}` : db``}
${!fav && user ? db`and items.username ilike ${userLike}` : db``}
${idsFilter}
${mimeSQL}
${(!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``}
@@ -1579,17 +1584,22 @@ const f0cklib = {
}
}
const parentRatingTags = tags.filter(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
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 hasSubRating = subTags.some(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
const effectiveSubTags = hasSubRating ? subTags : [...parentRatingTags, ...subTags];
const subIsNsfl = cfg.enable_nsfl && effectiveSubTags.some(t => t.id == nsfl_id || t.normalized === 'nsfl');
const subIsNsfw = effectiveSubTags.some(t => t.id == 2);
const subIsSfw = effectiveSubTags.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)))) {
if (excludedTags && excludedTags.length > 0 && effectiveSubTags.length > 0) {
if (effectiveSubTags.some(t => excludedTags.includes(t.normalized) || excludedTags.includes(lib.slugify(t.tag)))) {
return false;
}
}
@@ -1664,10 +1674,16 @@ const f0cklib = {
album = await Promise.all(albumRows.map(async (r, idx) => {
const order = idx;
const subSlug = r.slug || r.id;
const isYouTube = r.mime === 'video/youtube';
const subBase = r.dest.replace(/\.[^.]+$/, '');
const isAudio = (r.mime || '').startsWith('audio/');
let subCover = null;
let subThumb = `${cfg.websrv.paths.thumbnails}/${subBase}.webp`;
// YouTube sub-items: thumbnail is stored by album_item row id (not by filename)
let subThumb = isYouTube
? `${cfg.websrv.paths.thumbnails}/${r.id}.webp`
: `${cfg.websrv.paths.thumbnails}/${subBase}.webp`;
// Also try YouTube CDN thumbnail as fallback
const ytVideoId = isYouTube ? r.dest.replace('yt:', '') : null;
if (isAudio) {
const caFile = path.join(cfg.paths.ca, `${subBase}.webp`);
@@ -1729,13 +1745,15 @@ const f0cklib = {
}
let subTags = tagsBySubId.get(r.id) || [];
const hasSubRating = subTags.some(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
const combinedSubTags = hasSubRating ? subTags : [...parentRatingTags, ...subTags];
return {
id: r.id,
slug: r.slug,
subf0ck_id: subSlug,
dest: `${cfg.websrv.paths.images}/${r.dest}`,
src: `${cfg.websrv.paths.images}/${r.dest}`,
dest: isYouTube ? r.dest : `${cfg.websrv.paths.images}/${r.dest}`,
src: isYouTube ? `https://www.youtube.com/embed/${ytVideoId}?enablejsapi=1` : `${cfg.websrv.paths.images}/${r.dest}`,
filename: r.dest,
mime: r.mime,
size: lib.formatSize(subSizeNum),
@@ -1746,12 +1764,16 @@ const f0cklib = {
display_index: order + 1,
is_first: order === 0,
is_video: (r.mime || '').startsWith('video/'),
is_youtube: isYouTube,
is_audio: isAudio,
is_image: (r.mime || '').startsWith('image/'),
has_coverart: isAudio ? !!subCover : false,
coverart: isAudio ? subCover : null,
thumb: subThumb,
tags: subTags
// Use YouTube CDN thumb as fallback for strip thumbnails
thumb: isYouTube
? `https://img.youtube.com/vi/${ytVideoId}/mqdefault.jpg`
: subThumb,
tags: combinedSubTags
};
}));
@@ -1797,11 +1819,12 @@ const f0cklib = {
actitem.coverart = targetSub.coverart;
}
if (targetSub.tags && targetSub.tags.length > 0) {
effectiveItemTags = targetSub.tags;
const hasSubRating = targetSub.tags.some(t => t.id == 1 || t.id == 2 || (cfg.enable_nsfl && (t.id == nsfl_id || t.normalized === 'nsfl')));
effectiveItemTags = hasSubRating ? targetSub.tags : [...parentRatingTags, ...targetSub.tags];
} else if (reqIdx === 0) {
effectiveItemTags = tags;
} else {
effectiveItemTags = [];
effectiveItemTags = [...parentRatingTags];
}
}
}
@@ -1944,7 +1967,7 @@ const f0cklib = {
})(),
// og_description: include rating + uploader for bots (Matrix, Discord, etc.)
og_description: (() => {
const ratingLabel = isNsfl ? 'NSFL' : (isNsfw ? 'NSFW' : (isSfw ? 'SFW' : 'Untagged'));
const ratingLabel = isNsfl ? 'NSFL' : (isNsfw ? 'NSFW' : (isSfw ? 'SFW' : 'Unrated'));
const titlePart = actitem.title ? ` · "${actitem.title}"` : '';
return `${ratingLabel}${titlePart} · uploaded by ${actitem.username}`;
})(),
@@ -2012,7 +2035,8 @@ const f0cklib = {
return null;
}
}
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const user = rawUser ? decodeURI(rawUser) : null;
const userLike = user ? lib.escapeLike(user) : null;
const hall = rawHall || null;
// --- title: prefix — search items.title instead of the tags table ---
@@ -2079,7 +2103,7 @@ const f0cklib = {
left join tags on tags.id = tags_assign.tag_id
where
${db.unsafe(modequery)}
and "user".user ilike ${user}
and "user".user ilike ${userLike}
and items.active = true
and coalesce(items.visibility, 0) = 0
${mimeSQL}
@@ -2121,7 +2145,7 @@ const f0cklib = {
and items.active = true
and coalesce(items.visibility, 0) = 0
${tagFilter}
${user ? db`and items.username ilike ${user}` : db``}
${user ? db`and items.username ilike ${userLike}` : 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 || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
@@ -2223,7 +2247,8 @@ const f0cklib = {
return { items: [], total: 0 };
}
}
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const user = rawUser ? decodeURI(rawUser) : null;
const userLike = user ? lib.escapeLike(user) : null;
const hall = rawHall || null;
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
@@ -2277,7 +2302,7 @@ const f0cklib = {
INNER JOIN items ON favorites.item_id = items.id
INNER JOIN "user" ON "user".id = favorites.user_id
WHERE ${db.unsafe(modequery)}
AND "user".user ILIKE ${user}
AND "user".user ILIKE ${userLike}
AND items.active = true AND coalesce(items.visibility, 0) = 0
${mimeSQL} ${globalFilter}
ORDER BY items.id
@@ -2310,7 +2335,7 @@ const f0cklib = {
WHERE ${db.unsafe(modequery)}
AND items.active = true AND coalesce(items.visibility, 0) = 0
${tagFilter}
${user ? db`and items.username ilike ${user}` : db``}
${user ? db`and items.username ilike ${userLike}` : 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} ${globalFilter} ${excludeFilter}
ORDER BY items.id