hip
This commit is contained in:
@@ -1476,7 +1476,7 @@ const f0cklib = {
|
||||
]);
|
||||
console.log(`[GETF0CK_OPT] All queries finished in ${Date.now() - runTimings}ms`);
|
||||
|
||||
const link = lib.genLink({ user, tag, hall: (hall && typeof hall === 'object') ? hall.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: '', strict: false });
|
||||
const link = lib.genLink({ user, tag, hall: (hall && typeof hall === 'object') ? hall.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: '', strict: strict });
|
||||
// Override link for title searches — pagination must use the /tag/title:... prefix
|
||||
if (isTitleSearch && titleQuery) {
|
||||
link.main = `/tag/title:${encodeURIComponent(titleQuery)}/`;
|
||||
|
||||
+26
-11
@@ -170,13 +170,19 @@ export default (router, tpl) => {
|
||||
cdnUrls.push(`http://i.4cdn.org/${board}/${p.tim}${ext}`);
|
||||
cdnUrls.push(`/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}`);
|
||||
});
|
||||
const rehostMap = {};
|
||||
if (cdnUrls.length > 0) {
|
||||
try {
|
||||
const rows = await db`SELECT id, src FROM items WHERE src = ANY(${cdnUrls})`;
|
||||
const rows = await db`SELECT id, src, stamp FROM items WHERE src = ANY(${cdnUrls})`;
|
||||
rows.forEach(r => {
|
||||
const info = { id: r.id, stamp: r.stamp };
|
||||
rehosts[r.src] = r.id;
|
||||
rehostMap[r.src] = info;
|
||||
const m = r.src.match(/(\d{13,20})/);
|
||||
if (m) rehosts[m[1]] = r.id;
|
||||
if (m) {
|
||||
rehosts[m[1]] = r.id;
|
||||
rehostMap[m[1]] = info;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[CHAN] Rehost check error:', e.message);
|
||||
@@ -199,7 +205,10 @@ export default (router, tpl) => {
|
||||
const isVideo = ['.webm', '.mp4'].includes(ext);
|
||||
const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext);
|
||||
const externalMediaUrl = p.tim ? `https://i.4cdn.org/${board}/${p.tim}${ext}` : null;
|
||||
const localId = (p.tim && rehosts[p.tim]) || (externalMediaUrl && rehosts[externalMediaUrl]) || null;
|
||||
const rehostInfo = (p.tim && rehostMap[p.tim]) || (externalMediaUrl && rehostMap[externalMediaUrl]) || null;
|
||||
const localId = rehostInfo ? rehostInfo.id : null;
|
||||
const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time;
|
||||
const isoStr = new Date(effectiveTime * 1000).toISOString();
|
||||
|
||||
return {
|
||||
no: p.no,
|
||||
@@ -208,9 +217,10 @@ export default (router, tpl) => {
|
||||
com_formatted: formatComment(p.com || ''),
|
||||
name: p.name || 'Anonymous',
|
||||
trip: p.trip || '',
|
||||
time: p.time,
|
||||
timeago: lib.timeAgo(p.time * 1000, req.lang || 'en'),
|
||||
date_str: p.now || new Date(p.time * 1000).toLocaleString(),
|
||||
time: effectiveTime,
|
||||
timeago: lib.timeAgo(effectiveTime * 1000, req.lang || 'en'),
|
||||
date_str: (localId && rehostInfo?.stamp) ? new Date(effectiveTime * 1000).toLocaleString() : (p.now || new Date(effectiveTime * 1000).toLocaleString()),
|
||||
iso_str: isoStr,
|
||||
has_media: !!(p.tim && p.ext),
|
||||
tim: p.tim,
|
||||
ext: ext,
|
||||
@@ -240,6 +250,7 @@ export default (router, tpl) => {
|
||||
first_media_no: firstMediaPost ? firstMediaPost.no : null,
|
||||
rehosts_count: Object.keys(rehosts).length,
|
||||
session: req.session ? { ...req.session } : false,
|
||||
csrf_token: req.session?.csrf_token || '',
|
||||
domain: cfg.main.url.domain,
|
||||
tmp: null,
|
||||
page_meta: {
|
||||
@@ -306,7 +317,7 @@ export default (router, tpl) => {
|
||||
const rehosts = {};
|
||||
if (cdnUrls.length > 0) {
|
||||
try {
|
||||
const rows = await db`SELECT id, src FROM items WHERE src = ANY(${cdnUrls})`;
|
||||
const rows = await db`SELECT id, src, stamp FROM items WHERE src = ANY(${cdnUrls})`;
|
||||
rows.forEach(r => {
|
||||
rehosts[r.src] = r.id;
|
||||
const m = r.src.match(/(\d{13,20})/);
|
||||
@@ -333,13 +344,15 @@ export default (router, tpl) => {
|
||||
let localDest = null;
|
||||
let localThumb = null;
|
||||
let localTitle = null;
|
||||
let localStamp = null;
|
||||
let itemFavorites = [];
|
||||
let userHasFavorited = false;
|
||||
if (localId) {
|
||||
try {
|
||||
const localItem = await db`SELECT id, username, dest, title, mime, width, height FROM items WHERE id = ${localId} LIMIT 1`;
|
||||
const localItem = await db`SELECT id, username, dest, title, mime, width, height, stamp FROM items WHERE id = ${localId} LIMIT 1`;
|
||||
if (localItem.length > 0) {
|
||||
const li = localItem[0];
|
||||
localStamp = li.stamp;
|
||||
if (li.title) localTitle = li.title;
|
||||
if (li.dest) {
|
||||
localDest = `/b/${li.dest}`;
|
||||
@@ -368,6 +381,7 @@ export default (router, tpl) => {
|
||||
}
|
||||
|
||||
// Construct item matching f0ckm's standard item view expectations
|
||||
const effectiveStamp = (localId && localStamp) ? Number(localStamp) : targetPost.time;
|
||||
const item = {
|
||||
id: targetPost.no,
|
||||
slug: null,
|
||||
@@ -379,10 +393,10 @@ export default (router, tpl) => {
|
||||
height: targetPost.h || null,
|
||||
size: targetPost.fsize ? lib.formatSize(targetPost.fsize) : '0 B',
|
||||
username: targetPost.name || 'Anonymous',
|
||||
stamp: targetPost.time,
|
||||
stamp: effectiveStamp,
|
||||
timestamp: {
|
||||
timeago: lib.timeAgo(targetPost.time * 1000, req.lang || 'en'),
|
||||
timefull: new Date(targetPost.time * 1000).toISOString()
|
||||
timeago: lib.timeAgo(effectiveStamp * 1000, req.lang || 'en'),
|
||||
timefull: new Date(effectiveStamp * 1000).toISOString()
|
||||
},
|
||||
comment: targetPost.com ? targetPost.com.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '') : '',
|
||||
is_chan: true,
|
||||
@@ -466,6 +480,7 @@ export default (router, tpl) => {
|
||||
link,
|
||||
chan_thread: chanThreadMeta,
|
||||
session: req.session ? { ...req.session } : false,
|
||||
csrf_token: req.session?.csrf_token || '',
|
||||
domain: cfg.main.url.domain,
|
||||
tmp: null,
|
||||
hidePagination: true,
|
||||
|
||||
+36
-19
@@ -618,7 +618,7 @@ export default (router) => {
|
||||
return res.reply({ code: 401, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) });
|
||||
}
|
||||
|
||||
const { item_id, rating, tags, comment } = req.post || {};
|
||||
const { item_id, rating, tags, comment } = req.post || req.body || {};
|
||||
const itemId = parseInt(item_id, 10);
|
||||
if (!itemId || isNaN(itemId)) {
|
||||
return res.reply({ code: 400, body: JSON.stringify({ success: false, msg: 'Valid item_id is required' }) });
|
||||
@@ -636,11 +636,12 @@ export default (router) => {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) });
|
||||
}
|
||||
|
||||
const nsflTagRow = await db`SELECT id FROM tags WHERE normalized = 'nsfl' LIMIT 1`;
|
||||
const nsflId = nsflTagRow.length > 0 ? nsflTagRow[0].id : (cfg.nsfl_tag_id || 11517);
|
||||
|
||||
// 1. Update Rating if provided
|
||||
if (rating !== undefined && rating !== null && rating !== '') {
|
||||
const r = String(rating).toLowerCase().trim();
|
||||
const nsflTagRow = await db`SELECT id FROM tags WHERE normalized = 'nsfl' LIMIT 1`;
|
||||
const nsflId = nsflTagRow.length > 0 ? nsflTagRow[0].id : (cfg.nsfl_tag_id || 11517);
|
||||
|
||||
await db`
|
||||
DELETE FROM tags_assign
|
||||
@@ -658,22 +659,30 @@ export default (router) => {
|
||||
// If r === 'untagged', no rating tag is inserted
|
||||
}
|
||||
|
||||
// 2. Add Tags if provided
|
||||
const rawList = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(',') : []);
|
||||
const cleanTags = rawList.map(t => t.trim()).filter(t => t.length > 0 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase()));
|
||||
for (const tagName of cleanTags) {
|
||||
let tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`;
|
||||
if (tagRow.length === 0) {
|
||||
await db`INSERT INTO tags ${db({ tag: tagName }, 'tag')} ON CONFLICT DO NOTHING`;
|
||||
tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`;
|
||||
// 2. Sync Tags if provided
|
||||
if (tags !== undefined && tags !== null) {
|
||||
const rawList = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(',') : []);
|
||||
const cleanTags = rawList.map(t => t.trim()).filter(t => t.length > 0 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase()));
|
||||
|
||||
// Delete existing non-rating tags for this item by this user (or all non-rating tags if owner)
|
||||
await db`
|
||||
DELETE FROM tags_assign
|
||||
WHERE item_id = ${itemId}
|
||||
AND tag_id NOT IN (1, 2, ${nsflId})
|
||||
AND tag_id NOT IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl'))
|
||||
${isOwner ? db`` : db`AND user_id = ${session.id}`}
|
||||
`;
|
||||
|
||||
for (const tagName of cleanTags) {
|
||||
let tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`;
|
||||
if (tagRow.length === 0) {
|
||||
await db`INSERT INTO tags ${db({ tag: tagName }, 'tag')} ON CONFLICT DO NOTHING`;
|
||||
tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`;
|
||||
}
|
||||
if (tagRow.length) {
|
||||
await db`INSERT INTO tags_assign ${db({ item_id: itemId, tag_id: tagRow[0].id, user_id: session.id })} ON CONFLICT DO NOTHING`;
|
||||
}
|
||||
}
|
||||
if (tagRow.length) {
|
||||
await db`INSERT INTO tags_assign ${db({ item_id: itemId, tag_id: tagRow[0].id, user_id: session.id })} ON CONFLICT DO NOTHING`;
|
||||
}
|
||||
}
|
||||
if (cleanTags.length > 0) {
|
||||
const freshTags = await lib.getTags(itemId, session);
|
||||
await db.notify('tags', JSON.stringify({ item_id: itemId, fresh: true, tags: freshTags }));
|
||||
}
|
||||
|
||||
// 3. Add Comment if provided
|
||||
@@ -689,7 +698,15 @@ export default (router) => {
|
||||
}
|
||||
|
||||
const freshTags = await lib.getTags(itemId, session);
|
||||
await db.notify('tags', JSON.stringify({ item_id: itemId, fresh: true, tags: freshTags })).catch(() => {});
|
||||
|
||||
const ratingTag = freshTags.find(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized));
|
||||
const cleanTagObjects = freshTags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized)).map(t => ({
|
||||
id: t.id,
|
||||
tag: t.tag,
|
||||
normalized: t.normalized,
|
||||
badge: t.badge
|
||||
}));
|
||||
|
||||
return res.reply({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -697,7 +714,7 @@ export default (router) => {
|
||||
success: true,
|
||||
item_id: itemId,
|
||||
rating: ratingTag ? ratingTag.normalized : 'untagged',
|
||||
tags: freshTags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized)).map(t => t.tag)
|
||||
tags: cleanTagObjects
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -665,17 +665,19 @@ export default (router, tpl) => {
|
||||
// Used when the user receives a live notification while already viewing that item.
|
||||
// System-type notifications (item_deleted, deny, report, admin_pending) are excluded —
|
||||
// they require explicit user acknowledgment.
|
||||
router.post(/\/api\/notifications\/item\/(?<itemId>\d+)\/read/, async (req, res) => {
|
||||
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false }) });
|
||||
const itemId = req.params.itemId;
|
||||
router.post(/\/api\/notifications\/item\/(?<itemId>[a-zA-Z0-9_-]{11}|\d+)\/read/, async (req, res) => {
|
||||
if (!req.session?.id) return res.reply({ code: 401, body: JSON.stringify({ success: false }) });
|
||||
const rawItemId = req.params.itemId;
|
||||
const numericId = await f0cklib.resolveNumericItemId(rawItemId);
|
||||
if (!numericId) return res.reply({ code: 404, body: JSON.stringify({ success: false }) });
|
||||
const SYSTEM_TYPES = ['item_deleted', 'deny', 'admin_pending', 'report', 'warning'];
|
||||
console.log(`[NotificationRoute] Marking comment notifications for item ${itemId} as read for user ${req.session.id}`);
|
||||
console.log(`[NotificationRoute] Marking notifications for item ${numericId} (${rawItemId}) as read for user ${req.session.id}`);
|
||||
try {
|
||||
await db`
|
||||
UPDATE notifications
|
||||
SET is_read = true
|
||||
WHERE user_id = ${req.session.id}
|
||||
AND item_id = ${+itemId}
|
||||
AND item_id = ${numericId}
|
||||
AND NOT (type = ANY(${SYSTEM_TYPES}))
|
||||
`;
|
||||
return res.reply({
|
||||
|
||||
@@ -144,7 +144,7 @@ export default (router, tpl) => {
|
||||
});
|
||||
|
||||
// Main tags page
|
||||
router.get(/^\/tags$/, async (req, res) => {
|
||||
router.get(/^\/tags?\/?$/, async (req, res) => {
|
||||
const phrase = cfg.websrv.phrases[~~(Math.random() * cfg.websrv.phrases.length)];
|
||||
const mode = req.mode ?? 0;
|
||||
const query = req.url.qs || {};
|
||||
|
||||
Reference in New Issue
Block a user