svelte lol
This commit is contained in:
212
src/inc/routes/apiv2/svelte.mjs
Normal file
212
src/inc/routes/apiv2/svelte.mjs
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* f0ckm API v2 — JSON endpoints for the new SvelteKit frontend.
|
||||
*
|
||||
* These routes live alongside the existing flummpress routes and are
|
||||
* purely additive — nothing in the old app is changed or removed.
|
||||
*
|
||||
* Mounted at: /api/v2/
|
||||
*/
|
||||
import f0cklib from '../../routeinc/f0cklib.mjs';
|
||||
import cfg from '../../config.mjs';
|
||||
|
||||
const json = (res, body, code = 200) => {
|
||||
res.writeHead(code, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(body));
|
||||
};
|
||||
|
||||
export default (router, _tpl) => {
|
||||
|
||||
// ── GET /api/v2/session ───────────────────────────────────────────────────
|
||||
router.get('/api/v2/session', async (req, res) => {
|
||||
if (!req.session) {
|
||||
return json(res, { loggedIn: false, user: null });
|
||||
}
|
||||
return json(res, {
|
||||
loggedIn: true,
|
||||
user: {
|
||||
id: req.session.id,
|
||||
user: req.session.user,
|
||||
login: req.session.login,
|
||||
admin: !!req.session.admin,
|
||||
is_moderator: !!req.session.is_moderator,
|
||||
mode: req.session.mode ?? 3,
|
||||
theme: req.session.theme ?? 'f0ck',
|
||||
avatar: req.session.avatar ?? null,
|
||||
avatar_file: req.session.avatar_file ?? null,
|
||||
username_color: req.session.username_color ?? null,
|
||||
display_name: req.session.display_name ?? null,
|
||||
use_new_layout: req.session.use_new_layout ?? true,
|
||||
excluded_tags: req.session.excluded_tags ?? [],
|
||||
min_xd_score: req.session.min_xd_score ?? 0,
|
||||
uploads_remaining: req.session.uploads_remaining,
|
||||
pending_count: req.session.pending_count,
|
||||
language: req.session.language ?? null,
|
||||
disable_autoplay: !!req.session.disable_autoplay,
|
||||
disable_swiping: !!req.session.disable_swiping,
|
||||
csrf_token: req.session.csrf_token ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── GET /api/v2/items ─────────────────────────────────────────────────────
|
||||
router.get('/api/v2/items', async (req, res) => {
|
||||
const q = req.url.qs ?? {};
|
||||
const ratingsRaw = req.cookies?.ratings ?? q.ratings ?? null;
|
||||
const ratingsArr = ratingsRaw
|
||||
? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r))
|
||||
: null;
|
||||
|
||||
const mode = q.mode !== undefined ? +q.mode : req.mode;
|
||||
|
||||
try {
|
||||
const data = await f0cklib.getf0cks({
|
||||
page: q.page ? +q.page : 1,
|
||||
mode,
|
||||
ratings: ratingsArr,
|
||||
mime: q.mime ?? (req.cookies?.mime ?? null),
|
||||
tag: q.tag ?? null,
|
||||
hall: q.hall ?? null,
|
||||
user: q.user ?? null,
|
||||
userHall: q.userHall ?? null,
|
||||
userHallOwner: q.userHallOwner ?? null,
|
||||
fav: q.fav === 'true',
|
||||
random: q.random === '1' || req.cookies?.random_mode === '1',
|
||||
strict: q.strict === '1' || q.strict === 'true' || !!req.session?.strict_mode,
|
||||
session: !!req.session,
|
||||
user_id: req.session?.id ?? null,
|
||||
exclude: req.session?.excluded_tags ?? [],
|
||||
minXdScore: req.session?.min_xd_score ?? 0,
|
||||
newer: q.newer ?? null,
|
||||
tagger: q.tagger ?? null
|
||||
});
|
||||
|
||||
if (!data.success) {
|
||||
return json(res, { success: false, items: [], pagination: null, total: 0, link: null });
|
||||
}
|
||||
|
||||
return json(res, {
|
||||
success: true,
|
||||
items: data.items,
|
||||
pagination: data.pagination,
|
||||
total: data.total,
|
||||
link: data.link
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[API v2] /items error:', e);
|
||||
return json(res, { success: false, error: 'Internal error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /api/v2/items/:id ─────────────────────────────────────────────────
|
||||
router.get(/^\/api\/v2\/items\/(?<itemid>\d+)$/, async (req, res) => {
|
||||
const itemid = req.params.itemid;
|
||||
const q = req.url.qs ?? {};
|
||||
const ratingsRaw = req.cookies?.ratings ?? q.ratings ?? null;
|
||||
const ratingsArr = ratingsRaw
|
||||
? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r))
|
||||
: null;
|
||||
|
||||
const mode = q.mode !== undefined ? +q.mode : req.mode;
|
||||
|
||||
try {
|
||||
const data = await f0cklib.getf0ck({
|
||||
itemid,
|
||||
mode,
|
||||
ratings: ratingsArr,
|
||||
session: !!req.session,
|
||||
user_id: req.session?.id ?? null,
|
||||
user: q.user ?? null,
|
||||
tag: q.tag ?? null,
|
||||
hall: q.hall ?? null,
|
||||
userHall: q.userHall ?? null,
|
||||
userHallOwner: q.userHallOwner ?? null,
|
||||
mime: q.mime ?? (req.cookies?.mime ?? null),
|
||||
fav: q.fav === 'true',
|
||||
random: q.random === '1',
|
||||
strict: q.strict === '1' || !!req.session?.strict_mode,
|
||||
exclude: req.session?.excluded_tags ?? [],
|
||||
lang: req.lang ?? 'en'
|
||||
});
|
||||
|
||||
if (!data.success) {
|
||||
return json(res, { success: false, item: null, message: data.message }, 404);
|
||||
}
|
||||
|
||||
// Precompute hall display data (mirrors existing index route logic)
|
||||
if (data.item?.halls?.length) {
|
||||
const currentHallSlug = data.tmp?.hall
|
||||
? (typeof data.tmp.hall === 'object' ? data.tmp.hall.slug : data.tmp.hall)
|
||||
: null;
|
||||
data.item.primaryHall = data.item.halls.find(h => h.slug === currentHallSlug) || data.item.halls[0];
|
||||
data.item.otherHalls = data.item.halls.filter(h => h.slug !== data.item.primaryHall.slug);
|
||||
} else if (data.item) {
|
||||
data.item.primaryHall = null;
|
||||
data.item.otherHalls = [];
|
||||
}
|
||||
|
||||
// Subscription status
|
||||
const isSubscribed = req.session
|
||||
? await f0cklib.getSubscriptionStatus(req.session.id, itemid).catch(() => false)
|
||||
: false;
|
||||
|
||||
// xD Score
|
||||
if (req.session || !cfg.main.hide_comments_from_public) {
|
||||
const commentsForScore = await f0cklib.getComments(itemid, 'old', false);
|
||||
const xdScore = f0cklib.computeXdScore(commentsForScore);
|
||||
const xdMeta = f0cklib.xdScoreMeta(xdScore);
|
||||
data.item.xd_score = xdScore;
|
||||
data.item.xd_tier = xdMeta.tier;
|
||||
data.item.xd_label = xdMeta.label;
|
||||
} else {
|
||||
data.item.xd_score = 0;
|
||||
data.item.xd_tier = 0;
|
||||
data.item.xd_label = '';
|
||||
}
|
||||
|
||||
// Notify read
|
||||
if (req.session?.id) {
|
||||
f0cklib.markNotificationsRead(req.session.id, itemid).catch(() => {});
|
||||
}
|
||||
|
||||
return json(res, {
|
||||
success: true,
|
||||
item: data.item,
|
||||
pagination: data.pagination,
|
||||
link: data.link,
|
||||
isSubscribed
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[API v2] /items/:id error:', e);
|
||||
return json(res, { success: false, error: 'Internal error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /api/v2/random ────────────────────────────────────────────────────
|
||||
router.get('/api/v2/random', async (req, res) => {
|
||||
const ratingsRaw = req.cookies?.ratings ?? null;
|
||||
const ratingsArr = ratingsRaw
|
||||
? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r))
|
||||
: null;
|
||||
|
||||
try {
|
||||
const data = await f0cklib.getRandom({
|
||||
mode: req.mode,
|
||||
ratings: ratingsArr,
|
||||
session: !!req.session,
|
||||
mime: req.cookies?.mime ?? null
|
||||
});
|
||||
|
||||
if (!data.success) {
|
||||
return json(res, { success: false, message: data.message }, 404);
|
||||
}
|
||||
|
||||
const url = encodeURI(`${data.link.main}${data.link.path ?? ''}${data.itemid}${data.link.suffix ?? ''}`);
|
||||
return json(res, { success: true, id: data.itemid, url });
|
||||
} catch (e) {
|
||||
console.error('[API v2] /random error:', e);
|
||||
return json(res, { success: false, error: 'Internal error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -614,7 +614,9 @@ process.on('uncaughtException', err => {
|
||||
let _cachedRow = _scGet(_sessionHash);
|
||||
let user;
|
||||
if (_cachedRow) {
|
||||
user = [_cachedRow];
|
||||
// Shallow-clone the cached row so per-request mutations (theme, uploads_remaining,
|
||||
// pending_count, etc.) don't corrupt the shared cache entry.
|
||||
user = [{ ..._cachedRow }];
|
||||
} else {
|
||||
user = await db`
|
||||
select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user".banned, "user".ban_reason, "user".ban_expires, "user".force_password_change, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".mode, "user_options".theme, "user_options".fullscreen, "user_options".excluded_tags, "user_options".avatar, "user_options".avatar_file, "user_options".show_motd, "user_options".strict_mode, "user_options".show_background, "user_options".use_new_layout, "user_options".username_color, "user_options".font, "user_options".disable_autoplay, "user_options".disable_swiping, "user_options".description, "user_options".display_name, COALESCE("user_options".min_xd_score, 0) as min_xd_score, "user_options".ruffle_volume, "user_options".ruffle_background, "user_options".quote_emojis, "user_options".embed_youtube_in_comments, "user_options".hide_koepfe, "user_options".language, "user_options".use_alternative_infobox, "user_options".use_alternative_steuerung, "user_options".receive_system_notifications, "user_options".receive_user_notifications, "user_options".do_not_disturb, "user_options".comment_display_mode, "user_options".force_comment_display_mode
|
||||
@@ -624,7 +626,8 @@ process.on('uncaughtException', err => {
|
||||
where "user_sessions".session = ${_sessionHash}
|
||||
limit 1
|
||||
`;
|
||||
if (user.length > 0) _scSet(_sessionHash, user[0]);
|
||||
// Store a plain-object clone — keeps the cache entry free of per-request fields.
|
||||
if (user.length > 0) _scSet(_sessionHash, { ...user[0] });
|
||||
}
|
||||
|
||||
if (user.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user