gfdsgfds
This commit is contained in:
@@ -3745,6 +3745,7 @@ const f0cklib = {
|
||||
},
|
||||
|
||||
|
||||
resolveNumericItemId,
|
||||
computeBaseMode,
|
||||
getGlobalfilter,
|
||||
processMentions,
|
||||
@@ -3756,6 +3757,7 @@ const f0cklib = {
|
||||
clearCountCache: () => countCache.clear()
|
||||
};
|
||||
|
||||
export { resolveNumericItemId };
|
||||
export default f0cklib;
|
||||
|
||||
|
||||
|
||||
@@ -1904,32 +1904,47 @@ export default router => {
|
||||
`;
|
||||
|
||||
let favorited = false;
|
||||
if (existing.length > 0) {
|
||||
// del fav
|
||||
await db`
|
||||
delete from "favorites"
|
||||
where user_id = ${+req.session.id}
|
||||
and item_id = ${+postid}
|
||||
`;
|
||||
favorited = false;
|
||||
f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: -5.0 }).catch(() => {});
|
||||
if (req.session?.is_anon) {
|
||||
await logAnonActivity(req, { action: 'unfavorite', targetId: postid });
|
||||
}
|
||||
const reqAction = req.post?.action ?? req.body?.action;
|
||||
const reqFavorited = req.post?.favorited ?? req.body?.favorited;
|
||||
let shouldFav;
|
||||
if (reqAction === 'add' || reqAction === 'favorite' || reqFavorited === true || reqFavorited === 'true') {
|
||||
shouldFav = true;
|
||||
} else if (reqAction === 'delete' || reqAction === 'remove' || reqAction === 'unfavorite' || reqFavorited === false || reqFavorited === 'false') {
|
||||
shouldFav = false;
|
||||
} else {
|
||||
// add fav — ON CONFLICT DO NOTHING guards against rapid double-taps
|
||||
await db`
|
||||
insert into "favorites" ${db({
|
||||
item_id: +postid,
|
||||
user_id: +req.session.id
|
||||
}, 'item_id', 'user_id')}
|
||||
on conflict do nothing
|
||||
`;
|
||||
favorited = true;
|
||||
f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: 5.0 }).catch(() => {});
|
||||
if (req.session?.is_anon) {
|
||||
await logAnonActivity(req, { action: 'favorite', targetId: postid });
|
||||
shouldFav = existing.length === 0;
|
||||
}
|
||||
|
||||
if (!shouldFav) {
|
||||
if (existing.length > 0) {
|
||||
// del fav
|
||||
await db`
|
||||
delete from "favorites"
|
||||
where user_id = ${+req.session.id}
|
||||
and item_id = ${+postid}
|
||||
`;
|
||||
f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: -5.0 }).catch(() => {});
|
||||
if (req.session?.is_anon) {
|
||||
await logAnonActivity(req, { action: 'unfavorite', targetId: postid });
|
||||
}
|
||||
}
|
||||
favorited = false;
|
||||
} else {
|
||||
if (existing.length === 0) {
|
||||
// add fav — ON CONFLICT DO NOTHING guards against rapid double-taps
|
||||
await db`
|
||||
insert into "favorites" ${db({
|
||||
item_id: +postid,
|
||||
user_id: +req.session.id
|
||||
}, 'item_id', 'user_id')}
|
||||
on conflict do nothing
|
||||
`;
|
||||
f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: 5.0 }).catch(() => {});
|
||||
if (req.session?.is_anon) {
|
||||
await logAnonActivity(req, { action: 'favorite', targetId: postid });
|
||||
}
|
||||
}
|
||||
favorited = true;
|
||||
}
|
||||
|
||||
const favs = await db`
|
||||
@@ -1970,7 +1985,7 @@ export default router => {
|
||||
// If the caller is in anonymous mode, strip other users' identities
|
||||
const isCallerAnonymized = isAnonymizeSession(req.session);
|
||||
const clientFavs = isCallerAnonymized
|
||||
? sanitizedFavs.map(f => {
|
||||
? favs.map(f => {
|
||||
const isSelf = req.session && req.session.id && f.user_id && Number(f.user_id) === Number(req.session.id);
|
||||
if (isSelf) return f;
|
||||
return {
|
||||
|
||||
+92
-40
@@ -144,6 +144,48 @@ export default (router, tpl) => {
|
||||
}
|
||||
});
|
||||
|
||||
function formatPosts(posts, board, rehostMap, userFavSet, targetPostNo = null, lang = 'en') {
|
||||
return posts.map(p => {
|
||||
const ext = (p.ext || '').toLowerCase();
|
||||
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 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,
|
||||
sub: p.sub || '',
|
||||
com_raw: p.com || '',
|
||||
com_formatted: formatComment(p.com || ''),
|
||||
name: p.name || 'Anonymous',
|
||||
trip: p.trip || '',
|
||||
time: effectiveTime,
|
||||
timeago: lib.timeAgo(effectiveTime * 1000, lang),
|
||||
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,
|
||||
filename: p.filename ? `${p.filename}${ext}` : null,
|
||||
w: p.w,
|
||||
h: p.h,
|
||||
fsize: p.fsize ? lib.formatSize(p.fsize) : null,
|
||||
dest: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}` : null,
|
||||
thumb: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}s.jpg` : null,
|
||||
external_media_url: externalMediaUrl,
|
||||
is_video: isVideo,
|
||||
is_image: isImage,
|
||||
local_id: localId,
|
||||
rehosted: !!localId,
|
||||
user_has_favorited: localId ? userFavSet.has(Number(localId)) : false,
|
||||
is_onara_active: targetPostNo ? p.no === targetPostNo : false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// 2. GET /4/:board/:thread — Overview of all posts in that thread
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
@@ -200,44 +242,7 @@ export default (router, tpl) => {
|
||||
}
|
||||
|
||||
// Format posts for template
|
||||
const formattedPosts = posts.map(p => {
|
||||
const ext = (p.ext || '').toLowerCase();
|
||||
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 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,
|
||||
sub: p.sub || '',
|
||||
com_raw: p.com || '',
|
||||
com_formatted: formatComment(p.com || ''),
|
||||
name: p.name || 'Anonymous',
|
||||
trip: p.trip || '',
|
||||
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,
|
||||
filename: p.filename ? `${p.filename}${ext}` : null,
|
||||
w: p.w,
|
||||
h: p.h,
|
||||
fsize: p.fsize ? lib.formatSize(p.fsize) : null,
|
||||
dest: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}` : null,
|
||||
thumb: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}s.jpg` : null,
|
||||
external_media_url: externalMediaUrl,
|
||||
is_video: isVideo,
|
||||
is_image: isImage,
|
||||
local_id: localId,
|
||||
rehosted: !!localId,
|
||||
user_has_favorited: localId ? userFavSet.has(Number(localId)) : false
|
||||
};
|
||||
});
|
||||
const formattedPosts = formatPosts(posts, board, rehostMap, userFavSet, null, req.lang || 'en');
|
||||
|
||||
const firstMediaPost = formattedPosts.find(p => p.has_media);
|
||||
|
||||
@@ -315,13 +320,19 @@ export default (router, tpl) => {
|
||||
cdnUrls.push(`/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}`);
|
||||
});
|
||||
const rehosts = {};
|
||||
const rehostMap = {};
|
||||
if (cdnUrls.length > 0) {
|
||||
try {
|
||||
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);
|
||||
@@ -529,7 +540,48 @@ export default (router, tpl) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Standard full page render
|
||||
// Full page render: if Onara is active, render thread page with onara modal open
|
||||
const cfgOnara = cfg.onara !== undefined ? cfg.onara : cfg.websrv?.onara;
|
||||
const cookieOnara = req.cookies?.f0ck_onara !== undefined
|
||||
? (req.cookies.f0ck_onara === '1' || req.cookies.f0ck_onara === 'true')
|
||||
: (req.cookies?.onara !== undefined ? (req.cookies.onara === '1' || req.cookies.onara === 'true') : null);
|
||||
const isOnara = (cfgOnara !== undefined && cfgOnara !== null)
|
||||
? !!cfgOnara
|
||||
: (cookieOnara !== null ? cookieOnara : !!req.session?.onara);
|
||||
|
||||
if (isOnara) {
|
||||
const isModern = req.session?.use_new_layout;
|
||||
const partialTpl = isModern ? 'item-partial-modern' : 'item-partial-legacy';
|
||||
const itemHtml = tpl.render(partialTpl, data, req);
|
||||
|
||||
const formattedPosts = formatPosts(posts, board, rehostMap, userFavSet, targetPost.no, req.lang || 'en');
|
||||
const firstMediaPost = formattedPosts.find(p => p.has_media);
|
||||
|
||||
const threadViewData = {
|
||||
board,
|
||||
tid,
|
||||
op,
|
||||
posts: formattedPosts,
|
||||
media_count: mediaPosts.length,
|
||||
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: data.page_meta,
|
||||
is_onara_item: true,
|
||||
onara: true,
|
||||
onara_item_html: itemHtml,
|
||||
item: data.item
|
||||
};
|
||||
|
||||
return res.reply({
|
||||
body: tpl.render('chan/thread', threadViewData, req)
|
||||
});
|
||||
}
|
||||
|
||||
// Standard full page render (when Onara is disabled)
|
||||
return res.reply({
|
||||
body: tpl.render('item', data, req)
|
||||
});
|
||||
|
||||
@@ -632,7 +632,16 @@ export default (router) => {
|
||||
|
||||
const isOwner = !!(rows[0].username && session.user && rows[0].username.toLowerCase() === session.user.toLowerCase());
|
||||
const isAdmin = !!(session.admin || session.is_moderator);
|
||||
if (!isOwner && !isAdmin) {
|
||||
const isChanUser = !!(session.admin || (Array.isArray(session.groups) && session.groups.includes('4chan')));
|
||||
|
||||
const hasTagsOrComment = (tags && Array.isArray(tags) ? tags.length > 0 : (typeof tags === 'string' && tags.trim().length > 0)) ||
|
||||
(comment && typeof comment === 'string' && comment.trim().length > 0);
|
||||
|
||||
if (hasTagsOrComment && !isOwner && !isAdmin) {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Only owner can edit tags and comment' }) });
|
||||
}
|
||||
|
||||
if (!isOwner && !isAdmin && !isChanUser) {
|
||||
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user