69
This commit is contained in:
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)],
|
||||
|
||||
@@ -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
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
|
||||
@@ -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[])
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user