Files
shd.build/src/routes/database/[category]/+page.server.ts
T

1619 lines
64 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { databaseCategory } from '$lib/database/categories';
import type { DatabaseFact, DatabaseItem } from '$lib/database/items';
import type { GameIconName } from '$lib/game-icons';
import { gearBaseStatRange, weaponBaseRange } from '$lib/builder/loadout';
import {
gameDataIconUrl,
type GameDataIconType,
} from '$lib/server/game-data-icons';
import { managedImageUrl } from '$lib/server/managed-images';
import { getDatabase } from '$lib/server/database';
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import {
gameDataCorrectionHref,
gameDataDiscussionId,
gameDataTarget,
} from '$lib/game-data-targets';
import {
gameDataDiscussionSummary,
gameDataTargetExists,
} from '$lib/server/game-data-discussions';
import { getSessionUser } from '$lib/server/auth/session';
import { getPermissions } from '$lib/server/auth/authorization';
import { normalizeUserMarkdown, renderUserMarkdown } from '$lib/markdown';
import {
createUserContentReport,
getCommunityModerationPolicy,
moderateCommunityText,
recordCommunityModerationScan,
} from '$lib/server/community-moderation';
import { randomUUID } from 'node:crypto';
import { fail } from '@sveltejs/kit';
import { avatarUrl } from '$lib/avatar';
import {
orderProfileCommentThread,
prioritizeCommentConversation,
} from '$lib/server/profile-comment-thread';
import { recordIsacAudit } from '$lib/server/audit';
import { removePublicComment } from '$lib/server/comment-removal';
import { createNotification } from '$lib/server/notifications';
import type { Actions } from './$types';
import {
publicWeaponModSlots,
weaponModSlotDefinitionByName,
} from '$lib/weapons/mod-slots';
const number = (value: unknown) => {
const numeric = Number(value);
return Number.isFinite(numeric)
? Number.isInteger(numeric)
? String(numeric)
: numeric.toFixed(2).replace(/\.?0+$/, '')
: '—';
};
const valueWithUnit = (value: unknown, unit: unknown) =>
`${number(value)}${unit ? (unit === '%' ? '%' : ` ${unit}`) : ''}`;
const range = (min: unknown, max: unknown, unit: unknown = '') =>
`${valueWithUnit(min, unit)}–${valueWithUnit(max, unit)}`;
const list = (value: unknown, fallback = 'NONE') =>
typeof value === 'string' && value ? value : fallback;
const gearIcon = (slot: unknown) =>
String(slot).toLowerCase().replaceAll(' ', '_') as GameIconName;
const weaponIcon = (category: unknown) =>
String(category).toLowerCase() as GameIconName;
const iconUrl = (type: GameDataIconType, id: unknown, updatedAt: unknown) =>
updatedAt ? gameDataIconUrl(type, String(id), updatedAt) : '';
const parsedObject = (value: unknown): Record<string, unknown> => {
try {
const parsed = JSON.parse(String(value ?? '{}'));
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {};
} catch {
return {};
}
};
const talentCardItem = (
row: Record<string, any>,
mechanics: Array<Record<string, any>>,
weaponSlotOverride?: number,
): DatabaseItem => {
const talentCategory = String(row.category);
const isWeaponTalent = talentCategory === 'Weapon';
const normalizedCategory =
talentCategory === 'Vest' ? 'Body Armor' : talentCategory;
const weaponSlot =
weaponSlotOverride ??
(row.weapon_slot ? Number(row.weapon_slot) : undefined);
const slotLabel =
isWeaponTalent && weaponSlot
? `WEAPON ${weaponSlot}`
: normalizedCategory.toUpperCase();
const afterLines: DatabaseFact[] = [
...(row.duration_seconds
? [
{
label: 'EFFECT DURATION',
value: `${number(row.duration_seconds)} SECONDS`,
},
]
: []),
...(Number(row.max_stacks) > 1
? [{ label: 'MAXIMUM STACKS', value: String(row.max_stacks) }]
: []),
...(row.cooldown_seconds
? [
{
label: 'TALENT COOLDOWN',
value: `${number(row.cooldown_seconds)} SECONDS`,
},
]
: []),
];
const filterKey = isWeaponTalent ? 'weaponTalent' : 'talent';
const destination = isWeaponTalent ? 'weapons' : 'gear';
return {
id: String(row.id),
targetType: 'talent',
name: String(row.name),
kicker: `${slotLabel} TALENT`,
variant: 'talent',
iconUrl: iconUrl('talent', row.id, row.icon_updated_at ?? row.updated_at),
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
headerMetaIcon: isWeaponTalent ? 'smg' : gearIcon(normalizedCategory),
headerBadge:
isWeaponTalent && weaponSlot
? `${weaponSlot} · ${weaponSlot === 1 ? 'TOP' : 'BOTTOM'}`
: undefined,
badges: [
...(row.weapon_scope === 'Exotic'
? [{ label: 'EXOTIC', tone: 'exotic' }]
: []),
],
facts: [],
description: row.tier_i
? String(row.tier_i)
: 'Description is not available.',
secondaryDescription: row.tier_ii
? String(row.tier_ii)
: 'Description is not available.',
descriptionStyle: 'data',
afterLines,
actions: [
{
label: `FIND COMPATIBLE ${isWeaponTalent ? 'WEAPONS' : 'GEAR'}`,
href: `/database/${destination}?${filterKey}=${encodeURIComponent(String(row.id))}`,
},
],
};
};
export const load: PageServerLoad = async ({ params, url, cookies }) => {
const category = databaseCategory(params.category);
if (!category) error(404, 'Database category not found');
const database = await getDatabase();
const { search, pattern } = getRegistrySearch(url);
const searchArgs = search ? [pattern] : [];
const searchClause = search
? " AND item.name LIKE ? ESCAPE '\\' COLLATE NOCASE"
: '';
let queryArgs: unknown[] = searchArgs;
let filters: Array<{
key: string;
label: string;
kind: 'segmented' | 'searchable';
selected: string[];
options: Array<{
value: string;
label: string;
description?: string;
searchText?: string;
iconUrl?: string;
gameIcon?: GameIconName;
}>;
}> = [];
let countSql = '';
let rowsSql = '';
switch (category.slug) {
case 'gear': {
const [brandRows, talentRows] = await Promise.all([
(
await database.prepare(`SELECT brands.id,brands.name,
(SELECT GROUP_CONCAT(DISTINCT attributes.name)
FROM brand_set_effects effects
JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id
WHERE effects.brand_set_id=brands.id) AS bonus_attributes,
icons.updated_at FROM brand_sets brands
LEFT JOIN game_data_icons icons ON icons.entity_type='brand_set' AND icons.entity_id=brands.id
WHERE brands.active=1 ORDER BY brands.name COLLATE NOCASE`)
).all(),
(
await database.prepare(`SELECT talents.id,talents.name,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='I') AS tier_i_description,
icons.updated_at FROM talents
LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=talents.id
WHERE talents.active=1 AND talents.category IN ('Backpack','Body Armor','Vest')
ORDER BY talents.name COLLATE NOCASE`)
).all(),
]);
const slots = [
'Holster',
'Gloves',
'Backpack',
'Knee Pads',
'Body Armor',
'Mask',
];
const validBrands = new Set(brandRows.map((row) => String(row.id)));
const validTalents = new Set(talentRows.map((row) => String(row.id)));
const selectedSlots = url.searchParams
.getAll('slot')
.filter((value) => slots.includes(value));
const selectedBrands = url.searchParams
.getAll('brand')
.filter((value) => validBrands.has(value));
const selectedTalents = url.searchParams
.getAll('talent')
.filter((value) => validTalents.has(value));
const conditions = ['item.active=1'];
queryArgs = [];
if (search) {
conditions.push("item.name LIKE ? ESCAPE '\\' COLLATE NOCASE");
queryArgs.push(pattern);
}
if (selectedSlots.length) {
conditions.push(
`item.slot IN (${selectedSlots.map(() => '?').join(',')})`,
);
queryArgs.push(...selectedSlots);
}
if (selectedBrands.length) {
conditions.push(`EXISTS (SELECT 1 FROM gear_brand_sets filter_brands
WHERE filter_brands.gear_id=item.id AND filter_brands.brand_set_id IN (${selectedBrands.map(() => '?').join(',')}))`);
queryArgs.push(...selectedBrands);
}
if (selectedTalents.length) {
conditions.push(`EXISTS (SELECT 1 FROM gear_talents filter_talents
WHERE filter_talents.gear_id=item.id AND filter_talents.talent_id IN (${selectedTalents.map(() => '?').join(',')}))`);
queryArgs.push(...selectedTalents);
}
const where = conditions.join(' AND ');
countSql = `SELECT COUNT(*) AS total FROM gear item WHERE ${where}`;
rowsSql = `SELECT item.*,render.id AS render_id,render.updated_at AS render_updated_at
FROM gear item
LEFT JOIN managed_images render ON render.source_entity_type='gear_render' AND render.source_entity_id=item.id
WHERE ${where}
ORDER BY item.slot,item.tier,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
filters = [
{
key: 'slot',
label: 'GEAR SLOT',
kind: 'segmented',
selected: selectedSlots,
options: slots.map((slot) => ({
value: slot,
label: slot,
gameIcon: gearIcon(slot),
})),
},
{
key: 'brand',
label: 'BRAND SETS',
kind: 'searchable',
selected: selectedBrands,
options: brandRows.map((row) => ({
value: String(row.id),
label: String(row.name),
searchText: String(row.bonus_attributes ?? ''),
iconUrl: iconUrl('brand_set', row.id, row.updated_at),
})),
},
{
key: 'talent',
label: 'TALENTS',
kind: 'searchable',
selected: selectedTalents,
options: talentRows.map((row) => ({
value: String(row.id),
label: String(row.name),
description: String(row.tier_i_description ?? ''),
iconUrl: iconUrl('talent', row.id, row.updated_at),
})),
},
];
break;
}
case 'weapons': {
const talentRows = await (
await database.prepare(`SELECT DISTINCT talents.id,talents.name,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='I') AS tier_i_description,
icons.updated_at
FROM weapon_talent_options options
JOIN weapons source_weapons ON source_weapons.id=options.weapon_id AND source_weapons.active=1
JOIN talents ON talents.id=options.talent_id AND talents.active=1
LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=talents.id
ORDER BY talents.name COLLATE NOCASE`)
).all();
const weaponTypes = ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG'];
const damageTypes = ['Blast', 'Pierce', 'Shredding'];
const validTalents = new Set(talentRows.map((row) => String(row.id)));
const selectedTypes = url.searchParams
.getAll('weaponType')
.filter((value) => weaponTypes.includes(value));
const selectedDamageTypes = url.searchParams
.getAll('damageType')
.filter((value) => damageTypes.includes(value));
const selectedTalents = url.searchParams
.getAll('weaponTalent')
.filter((value) => validTalents.has(value));
const conditions = ['item.active=1'];
queryArgs = [];
if (search) {
conditions.push("item.name LIKE ? ESCAPE '\\' COLLATE NOCASE");
queryArgs.push(pattern);
}
if (selectedTypes.length) {
conditions.push(
`item.category IN (${selectedTypes.map(() => '?').join(',')})`,
);
queryArgs.push(...selectedTypes);
}
if (selectedDamageTypes.length) {
conditions.push(
`item.damage_type IN (${selectedDamageTypes.map(() => '?').join(',')})`,
);
queryArgs.push(...selectedDamageTypes);
}
if (selectedTalents.length) {
conditions.push(`EXISTS (SELECT 1 FROM weapon_talent_options filter_talents
WHERE filter_talents.weapon_id=item.id AND filter_talents.talent_id IN (${selectedTalents.map(() => '?').join(',')}))`);
queryArgs.push(...selectedTalents);
}
const where = conditions.join(' AND ');
countSql = `SELECT COUNT(*) AS total FROM weapons item WHERE ${where}`;
rowsSql = `SELECT item.*,render.id AS render_id,render.updated_at AS render_updated_at,
native.damage_min,native.damage_max,native.reference_level,
(SELECT COUNT(*) FROM weapon_talent_options options WHERE options.weapon_id=item.id AND options.talent_slot=1) AS talent_1_count,
(SELECT COUNT(*) FROM weapon_talent_options options WHERE options.weapon_id=item.id AND options.talent_slot=2) AS talent_2_count
FROM weapons item
LEFT JOIN weapon_tier_ranges native ON native.weapon_id=item.id AND native.tier=item.native_tier
LEFT JOIN managed_images render ON render.source_entity_type='weapon_render' AND render.source_entity_id=item.id
WHERE ${where}
ORDER BY item.is_exotic DESC,item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
filters = [
{
key: 'weaponType',
label: 'WEAPON TYPE',
kind: 'segmented',
selected: selectedTypes,
options: weaponTypes.map((type) => ({
value: type,
label: type,
gameIcon: weaponIcon(type),
})),
},
{
key: 'damageType',
label: 'DAMAGE TYPE',
kind: 'segmented',
selected: selectedDamageTypes,
options: damageTypes.map((type) => ({
value: type,
label: type,
gameIcon: weaponIcon(type),
})),
},
{
key: 'weaponTalent',
label: 'TALENT POOL',
kind: 'searchable',
selected: selectedTalents,
options: talentRows.map((row) => ({
value: String(row.id),
label: String(row.name),
description: String(row.tier_i_description ?? ''),
iconUrl: iconUrl('talent', row.id, row.updated_at),
})),
},
];
break;
}
case 'weapon-mods':
countSql = `SELECT COUNT(*) AS total FROM weapon_mods item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,weapons.name AS exotic_weapon,
render.id AS render_id,render.updated_at AS render_updated_at,
GROUP_CONCAT(DISTINCT categories.weapon_category) AS categories,
(SELECT COUNT(*) FROM weapon_mod_secondary_options) AS secondary_count,
(SELECT COUNT(*) FROM weapon_mod_effects effects WHERE effects.weapon_mod_id=item.id) AS effect_count
FROM weapon_mods item
LEFT JOIN weapons ON weapons.id=item.exotic_weapon_id
LEFT JOIN weapon_mod_compatible_categories categories ON categories.weapon_mod_id=item.id
LEFT JOIN managed_images render ON render.source_entity_type='weapon_mod_render' AND render.source_entity_id=item.id
WHERE item.active=1${searchClause}
GROUP BY item.id ORDER BY item.slot_type,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'gear-mods':
countSql = `SELECT COUNT(*) AS total FROM gear_mods item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,render.id AS render_id,render.updated_at AS render_updated_at,
(SELECT COUNT(*) FROM gear_mod_secondary_options) AS secondary_count,
(SELECT MIN(primary_level_1_min) FROM gear_mod_quality_values quality WHERE quality.gear_mod_id=item.id) AS level_1_floor,
(SELECT MAX(primary_level_20_max) FROM gear_mod_quality_values quality WHERE quality.gear_mod_id=item.id) AS level_20_ceiling
FROM gear_mods item
LEFT JOIN managed_images render ON render.source_entity_type='gear_mod_render' AND render.source_entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'os-protocols':
countSql = `SELECT COUNT(*) AS total FROM os_protocols item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,attributes.name AS primary_stat,attributes.unit AS primary_unit,
primary_effect.base_value,talents.name AS talent_name,talents.description_template AS talent_description,icons.updated_at AS icon_updated_at,
render.id AS render_id,render.updated_at AS render_updated_at
FROM os_protocols item
LEFT JOIN os_protocol_primary_effects primary_effect ON primary_effect.protocol_id=item.id
LEFT JOIN stat_attributes attributes ON attributes.id=primary_effect.stat_attribute_id
LEFT JOIN os_protocol_talents talents ON talents.protocol_id=item.id
LEFT JOIN game_data_icons icons ON icons.entity_type='os_protocol' AND icons.entity_id=item.id
LEFT JOIN managed_images render ON render.source_entity_type='os_protocol_render' AND render.source_entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.quality,item.primary_attribute_type,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'talents': {
const talentSlots = [
{ value: 'Backpack', label: 'Backpack', gameIcon: 'backpack' as const },
{
value: 'Body Armor',
label: 'Body Armor',
gameIcon: 'body_armor' as const,
},
{
value: 'Weapon 1',
label: 'Weapon 1 · Top',
gameIcon: 'smg' as const,
},
{
value: 'Weapon 2',
label: 'Weapon 2 · Bottom',
gameIcon: 'smg' as const,
},
];
const selectedTalentSlots = url.searchParams
.getAll('talentSlot')
.filter((value) => talentSlots.some((slot) => slot.value === value));
const conditions = ['item.active=1'];
queryArgs = [];
if (search) {
conditions.push("item.name LIKE ? ESCAPE '\\' COLLATE NOCASE");
queryArgs.push(pattern);
}
if (selectedTalentSlots.length) {
const slotConditions = selectedTalentSlots.map((slot) => {
if (slot === 'Backpack') return "item.category='Backpack'";
if (slot === 'Body Armor')
return "item.category IN ('Body Armor','Vest')";
queryArgs.push(slot === 'Weapon 1' ? 1 : 2);
return `item.category='Weapon' AND EXISTS (
SELECT 1 FROM weapon_talent_slots filter_slots
WHERE filter_slots.talent_id=item.id AND filter_slots.slot=?
)`;
});
conditions.push(
`(${slotConditions.map((condition) => `(${condition})`).join(' OR ')})`,
);
}
const where = conditions.join(' AND ');
countSql = `SELECT COUNT(*) AS total FROM talents item WHERE ${where}`;
rowsSql = `SELECT item.*,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=item.id AND tiers.tier='I') AS tier_i,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=item.id AND tiers.tier='II') AS tier_ii,
(SELECT slot FROM weapon_talent_slots slots WHERE slots.talent_id=item.id) AS weapon_slot,
(SELECT scope FROM talent_weapon_scopes scopes WHERE scopes.talent_id=item.id) AS weapon_scope,
icons.updated_at AS icon_updated_at,render.id AS render_id,render.updated_at AS render_updated_at
FROM talents item
LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=item.id
LEFT JOIN managed_images render ON render.source_entity_type='talent_render' AND render.source_entity_id=item.id
WHERE ${where}
ORDER BY item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
filters = [
{
key: 'talentSlot',
label: 'TALENT SLOT',
kind: 'segmented',
selected: selectedTalentSlots,
options: talentSlots,
},
];
break;
}
case 'brands':
countSql = `SELECT COUNT(*) AS total FROM brand_sets item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,icons.updated_at AS icon_updated_at,render.id AS render_id,render.updated_at AS render_updated_at
FROM brand_sets item
LEFT JOIN game_data_icons icons ON icons.entity_type='brand_set' AND icons.entity_id=item.id
LEFT JOIN managed_images render ON render.source_entity_type='brand_set_render' AND render.source_entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'skill-chips':
countSql = `SELECT COUNT(*) AS total FROM skill_chip_sets item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,bonus.min_value,bonus.max_value,attributes.name AS bonus_stat,attributes.unit AS bonus_unit,
talents.name AS talent_name,talents.description_template AS talent_description,
icons.updated_at AS icon_updated_at,render.id AS render_id,render.updated_at AS render_updated_at
FROM skill_chip_sets item
LEFT JOIN skill_chip_two_piece_bonuses bonus ON bonus.skill_chip_set_id=item.id
LEFT JOIN stat_attributes attributes ON attributes.id=bonus.stat_attribute_id
LEFT JOIN skill_chip_talents talents ON talents.skill_chip_set_id=item.id
LEFT JOIN game_data_icons icons ON icons.entity_type='skill_chip' AND icons.entity_id=item.id
LEFT JOIN managed_images render ON render.source_entity_type='skill_chip_render' AND render.source_entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'specializations':
countSql = `SELECT COUNT(*) AS total FROM specializations item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,
(SELECT COUNT(*) FROM specialization_focuses focus WHERE focus.specialization_id=item.id AND focus.active=1) AS focus_count,
(SELECT COUNT(*) FROM specialization_focuses focus WHERE focus.specialization_id=item.id AND focus.builder_ready=1 AND focus.active=1) AS ready_focus_count,
(SELECT COUNT(*) FROM specialization_abilities ability WHERE ability.specialization_id=item.id AND ability.active=1) AS ability_count,
(SELECT COUNT(*) FROM specialization_tree_nodes node
JOIN specialization_focuses node_focus ON node_focus.id=node.focus_id
WHERE node_focus.specialization_id=item.id AND node_focus.active=1 AND node.active=1) AS node_count,
(SELECT GROUP_CONCAT(name, ', ') FROM (
SELECT focus.name FROM specialization_focuses focus
WHERE focus.specialization_id=item.id AND focus.active=1
ORDER BY focus.display_order,focus.name COLLATE NOCASE
)) AS focus_names,
(SELECT GROUP_CONCAT(name, ', ') FROM (
SELECT ability.name FROM specialization_abilities ability
WHERE ability.specialization_id=item.id AND ability.active=1
ORDER BY ability.name COLLATE NOCASE
)) AS ability_names,
icons.updated_at AS icon_updated_at,render.id AS render_id,render.updated_at AS render_updated_at
FROM specializations item
LEFT JOIN game_data_icons icons ON icons.entity_type='specialization' AND icons.entity_id=item.id
LEFT JOIN managed_images render ON render.source_entity_type='specialization_render' AND render.source_entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
}
const total = Number(
(await (await database.prepare(countSql)).get(...queryArgs))?.total ?? 0,
);
const pagination = getPagination(url, total);
const rows = await (
await database.prepare(rowsSql)
).all(...queryArgs, pagination.perPage, pagination.offset);
const protocolTalentEffects =
category.slug === 'os-protocols' && rows.length
? await (
await database.prepare(`SELECT talents.protocol_id,effects.base_value,attributes.unit
FROM os_protocol_talents talents
JOIN os_protocol_talent_effects effects ON effects.talent_id=talents.id
JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id
WHERE talents.protocol_id IN (${rows.map(() => '?').join(',')})
ORDER BY talents.protocol_id,effects.sort_order`)
).all(...rows.map((row) => row.id))
: [];
const weaponRelations =
category.slug === 'weapons' && rows.length
? await (
await database.prepare(`SELECT options.weapon_id,options.talent_slot,talents.*,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='I') AS tier_i,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='II') AS tier_ii,
icons.updated_at AS icon_updated_at
FROM weapon_talent_options options
JOIN talents ON talents.id=options.talent_id AND talents.active=1
LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=talents.id
WHERE options.weapon_id IN (${rows.map(() => '?').join(',')})
ORDER BY options.talent_slot,talents.name COLLATE NOCASE`)
).all(...rows.map((row) => row.id))
: [];
const weaponModSlotRows =
category.slug === 'weapons' && rows.length
? await (
await database.prepare(`SELECT weapon_id,tier,slot_type FROM weapon_mod_slots
WHERE weapon_id IN (${rows.map(() => '?').join(',')})
ORDER BY weapon_id,tier,slot_type`)
).all(...rows.map((row) => row.id))
: [];
const weaponModSlotIconRows =
category.slug === 'weapons'
? await (
await database.prepare(
"SELECT entity_id,updated_at FROM game_data_icons WHERE entity_type='weapon_mod_slot'",
)
).all()
: [];
const gearRelations =
category.slug === 'gear' && rows.length
? await (
await database.prepare(`SELECT relation.*,icons.updated_at
FROM (
SELECT links.gear_id,'brand' AS kind,brands.id,brands.name,
NULL AS category,NULL AS cooldown_seconds,NULL AS duration_seconds,NULL AS max_stacks,
NULL AS tier_i,NULL AS tier_ii
FROM gear_brand_sets links JOIN brand_sets brands ON brands.id=links.brand_set_id
WHERE brands.active=1
UNION ALL
SELECT links.gear_id,'talent' AS kind,talents.id,talents.name,
talents.category,talents.cooldown_seconds,talents.duration_seconds,talents.max_stacks,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='I') AS tier_i,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='II') AS tier_ii
FROM gear_talents links JOIN talents ON talents.id=links.talent_id
WHERE talents.active=1
) relation
LEFT JOIN game_data_icons icons ON icons.entity_id=relation.id
AND icons.entity_type=CASE relation.kind WHEN 'brand' THEN 'brand_set' ELSE 'talent' END
WHERE relation.gear_id IN (${rows.map(() => '?').join(',')})
ORDER BY relation.kind,relation.name COLLATE NOCASE`)
).all(...rows.map((row) => row.id))
: [];
const gearBrandEffects =
category.slug === 'gear' &&
gearRelations.some((relation) => relation.kind === 'brand')
? await (
await database.prepare(`SELECT effects.brand_set_id,effects.pieces_required,effects.value,
attributes.name,attributes.unit FROM brand_set_effects effects
JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id
WHERE effects.brand_set_id IN (${gearRelations
.filter((relation) => relation.kind === 'brand')
.map(() => '?')
.join(',')})
ORDER BY effects.pieces_required`)
).all(
...gearRelations
.filter((relation) => relation.kind === 'brand')
.map((relation) => relation.id),
)
: [];
const brandEffects =
category.slug === 'brands' && rows.length
? await (
await database.prepare(`SELECT effects.brand_set_id,effects.pieces_required,effects.value,
attributes.name,attributes.unit FROM brand_set_effects effects
JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id
WHERE effects.brand_set_id IN (${rows.map(() => '?').join(',')}) ORDER BY effects.pieces_required`)
).all(...rows.map((row) => row.id))
: [];
const talentIds = [
...new Set(
category.slug === 'talents'
? rows.map((row) => String(row.id))
: category.slug === 'weapons'
? weaponRelations.map((relation) => String(relation.id))
: category.slug === 'gear'
? gearRelations
.filter((relation) => relation.kind === 'talent')
.map((relation) => String(relation.id))
: [],
),
];
const talentMechanics = talentIds.length
? await (
await database.prepare(`SELECT rules.source_id AS talent_id,rules.id AS rule_id,
clauses.id AS clause_id,clauses.label AS clause_label,clauses.event_kind,
clauses.conditions_json,clauses.reducer_json,clauses.cooldown_seconds,
outcomes.variant_key,outcomes.outcome_key,outcomes.application_kind,outcomes.effect_scope,
outcomes.value,outcomes.damage_basis,attributes.name AS attribute_name,attributes.unit AS attribute_unit,
outcomes.sort_order
FROM conditional_effect_rules rules
JOIN conditional_effect_clauses clauses ON clauses.rule_id=rules.id AND clauses.active=1
LEFT JOIN conditional_effect_outcomes outcomes ON outcomes.clause_id=clauses.id
LEFT JOIN stat_attributes attributes ON attributes.id=outcomes.stat_attribute_id
WHERE rules.source_type='talent' AND rules.configured=1
AND rules.source_id IN (${talentIds.map(() => '?').join(',')})
ORDER BY rules.source_id,clauses.sort_order,outcomes.outcome_key,outcomes.sort_order`)
).all(...talentIds)
: [];
const mechanicsFor = (talentId: unknown) =>
talentMechanics.filter(
(mechanic) => String(mechanic.talent_id) === String(talentId),
);
const items: DatabaseItem[] = rows.map((row) => {
switch (category.slug) {
case 'gear': {
const relations = gearRelations.filter(
(relation) => relation.gear_id === row.id,
);
const level50Health = gearBaseStatRange(
row.base_health_min,
row.base_health_max,
50,
);
const level50Armor =
row.base_armor_min === null
? null
: gearBaseStatRange(row.base_armor_min, row.base_armor_max, 50);
const relatedItems = (
kind: 'brand' | 'talent',
type: GameDataIconType,
) =>
relations
.filter((relation) => relation.kind === kind)
.map((relation) => {
const relationIcon = iconUrl(
type,
relation.id,
relation.updated_at,
);
const effects =
kind === 'brand'
? gearBrandEffects.filter(
(effect) => effect.brand_set_id === relation.id,
)
: [];
return {
id: String(relation.id),
name: String(relation.name),
iconUrl: relationIcon,
preview:
kind === 'brand'
? {
id: String(relation.id),
targetType: 'brand_set',
name: String(relation.name),
kicker: 'BRAND SET // EQUIPMENT',
iconUrl: relationIcon,
factLayout: 'stack' as const,
actions: [
{
label: 'FIND GEAR WITH THIS BRAND',
href: `/database/gear?brand=${encodeURIComponent(String(relation.id))}`,
},
],
facts: effects.map((effect) => ({
label: `${effect.pieces_required} PIECES`,
value: `${effect.name} · +${valueWithUnit(effect.value, effect.unit)}`,
})),
}
: talentCardItem(
{
...relation,
icon_updated_at: relation.updated_at,
},
mechanicsFor(relation.id),
),
};
});
const iconGroups = [
{
label: 'BRAND OPTIONS',
items: relatedItems('brand', 'brand_set'),
emptyLabel: 'NO BRANDS ASSIGNED',
},
...(['Backpack', 'Body Armor'].includes(String(row.slot))
? [
{
label: 'AVAILABLE TALENTS',
items: relatedItems('talent', 'talent'),
emptyLabel: 'NO TALENTS ASSIGNED',
},
]
: []),
];
return {
id: String(row.id),
name: String(row.name),
kicker: `${row.slot} // T${row.tier}`,
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
gameIcon: gearIcon(row.slot),
headerBadges: [
`${Number(row.tier) === 0 ? 1 : 2} MOD SLOT${Number(row.tier) === 0 ? '' : 'S'}`,
],
headerBadge: `T${row.tier}`,
factLayout: 'stack' as const,
facts: [
{
label: 'BASE HEALTH',
value: `${level50Health.min.toLocaleString('en-US')}–${level50Health.max.toLocaleString('en-US')}`,
emphasis: 'primary' as const,
note: 'AT LEVEL 50',
},
...(level50Armor === null
? []
: [
{
label: 'BASE ARMOR',
value: `${level50Armor.min.toLocaleString('en-US')}–${level50Armor.max.toLocaleString('en-US')}`,
emphasis: 'primary' as const,
note: 'AT LEVEL 50',
},
]),
],
iconGroups,
};
}
case 'weapons': {
const relatedTalents = weaponRelations.filter(
(relation) => relation.weapon_id === row.id,
);
const modSlots = publicWeaponModSlots(
Number(row.native_tier),
[0, 1, 2].map((tier) => ({
tier,
slots: weaponModSlotRows
.filter(
(slot) =>
slot.weapon_id === row.id && Number(slot.tier) === tier,
)
.map((slot) => String(slot.slot_type)),
})),
).map((slot) => {
const definition = weaponModSlotDefinitionByName(slot.slot);
const icon = definition
? weaponModSlotIconRows.find(
(row) => row.entity_id === definition.id,
)
: null;
return {
...slot,
iconUrl:
definition && icon
? gameDataIconUrl(
'weapon_mod_slot',
definition.id,
icon.updated_at,
)
: '',
};
});
const talentItems = (slot: number) =>
relatedTalents
.filter((relation) => Number(relation.talent_slot) === slot)
.map((relation) => {
const talentIcon = iconUrl(
'talent',
relation.id,
relation.icon_updated_at,
);
return {
id: String(relation.id),
name: String(relation.name),
iconUrl: talentIcon,
preview: talentCardItem(
relation,
mechanicsFor(relation.id),
slot,
),
};
});
const level50Damage =
row.damage_min === null
? null
: weaponBaseRange(row.damage_min, row.damage_max, 50);
return {
id: String(row.id),
name: String(row.name),
kicker: `${row.category} // ${row.damage_type}`,
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
gameIcon: weaponIcon(row.category),
headerBadges: [
`T${row.native_tier}`,
...(Number(row.is_exotic) ? ['EXOTIC'] : []),
],
headerMetaIcon: weaponIcon(row.damage_type),
factLayout: 'stack' as const,
facts: [
{
label: 'DAMAGE',
value: level50Damage
? `${level50Damage.min.toLocaleString('en-US')}–${level50Damage.max.toLocaleString('en-US')}`
: 'NOT CONFIGURED',
emphasis: 'primary' as const,
gameIcon: weaponIcon(row.damage_type),
note: 'AT LEVEL 50',
},
{
label: 'RATE / MAG',
value: `${number(row.rpm)} RPM · ${number(row.magazine)} MAG`,
},
{ label: 'RELOAD', value: valueWithUnit(row.reload_speed, 's') },
{
label: 'HANDLING',
value: '',
meters: [
{ label: 'ACCURACY', value: Number(row.accuracy) || 0 },
{ label: 'STABILITY', value: Number(row.stability) || 0 },
],
},
],
weaponModSlots: modSlots,
iconGroups: [
{
label: 'TALENT 1 POOL',
items: talentItems(1),
emptyLabel: 'NO TALENTS ASSIGNED',
},
{
label: 'TALENT 2 POOL',
items: talentItems(2),
emptyLabel: 'NO TALENTS ASSIGNED',
},
],
};
}
case 'weapon-mods':
return {
id: String(row.id),
name: String(row.name),
kicker: `${row.slot_type} // WEAPON MOD`,
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
badges: row.exotic_weapon_id
? [{ label: 'EXOTIC', tone: 'exotic' }]
: [{ label: 'STANDARD → HIGH-END' }],
facts: [
{ label: 'LEVELS', value: '01–20' },
{
label: 'SECONDARY POOL',
value: row.exotic_weapon_id
? 'FIXED ATTRIBUTES'
: `${row.secondary_count} OPTIONS`,
},
{
label: 'MOD EFFECTS',
value: row.exotic_weapon_id ? 'FIXED' : String(row.effect_count),
},
],
lines: [
{
label: row.exotic_weapon_id
? 'ASSIGNED WEAPON'
: 'WEAPON CATEGORIES',
value: row.exotic_weapon_id
? list(row.exotic_weapon)
: list(row.categories).toUpperCase(),
},
],
};
case 'gear-mods':
return {
id: String(row.id),
name: String(row.name),
kicker: 'GEAR MOD // STANDARD QUALITIES',
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
badges: [{ label: 'STANDARD → HIGH-END' }],
facts: [
{ label: 'LEVELS', value: '01–20' },
{ label: 'PRIMARY OPTIONS', value: '5' },
{
label: 'SECONDARY POOL',
value: `${row.secondary_count} OPTIONS`,
},
],
lines: [
{
label: 'OBSERVED PRIMARY ENVELOPE',
value: `${number(row.level_1_floor)} → ${number(row.level_20_ceiling)}`,
},
],
};
case 'os-protocols':
return {
id: String(row.id),
name: String(row.name),
kicker: `${row.primary_attribute_type} // OS PROTOCOL`,
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
iconUrl: iconUrl('os_protocol', row.id, row.icon_updated_at),
topBorderTone: String(row.quality).toLowerCase().replace('-', ''),
headerBadge: String(row.quality).toUpperCase(),
headerBadgeTone: String(row.quality).toLowerCase().replace('-', ''),
factLayout: 'stack' as const,
facts: [],
protocolPreview: {
primaryStat: String(row.primary_stat ?? 'NOT CONFIGURED'),
primaryUnit: String(row.primary_unit ?? ''),
primaryBaseValue: Number(row.base_value),
talentDescription: row.talent_description
? String(row.talent_description)
: undefined,
talentEffects: protocolTalentEffects
.filter((effect) => effect.protocol_id === row.id)
.map((effect) => ({
baseValue: Number(effect.base_value),
unit: String(effect.unit ?? ''),
})),
},
description: row.talent_description
? String(row.talent_description)
: undefined,
};
case 'talents':
return talentCardItem(row, mechanicsFor(row.id));
case 'brands': {
const effects = brandEffects.filter(
(effect) => effect.brand_set_id === row.id,
);
return {
id: String(row.id),
name: String(row.name),
kicker: 'BRAND SET // EQUIPMENT',
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
iconUrl: iconUrl('brand_set', row.id, row.icon_updated_at),
factLayout: 'stack' as const,
actions: [
{
label: 'FIND GEAR WITH THIS BRAND',
href: `/database/gear?brand=${encodeURIComponent(String(row.id))}`,
},
],
facts: effects.map((effect) => ({
label: `${effect.pieces_required} PIECES`,
value: `${effect.name} · +${valueWithUnit(effect.value, effect.unit)}`,
})),
};
}
case 'skill-chips':
return {
id: String(row.id),
name: String(row.name),
kicker: 'SKILL CHIP SET // 2–3 PIECES',
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
iconUrl: iconUrl('skill_chip', row.id, row.icon_updated_at),
badges: [{ label: 'SUPERIOR' }, { label: 'HIGH-END' }],
facts: [
{
label: '2-PIECE BONUS',
value: row.bonus_stat
? `${row.bonus_stat} · ${range(row.min_value, row.max_value, row.bonus_unit)}`
: 'NOT CONFIGURED',
},
],
lines: [
{
label: '3-PIECE TALENT',
value: list(row.talent_name, 'NOT CONFIGURED'),
},
],
description: row.talent_description
? String(row.talent_description)
: undefined,
};
case 'specializations':
return {
id: String(row.id),
name: String(row.name),
kicker: 'SPECIALIZATION // FOCUS PATHS',
renderUrl: row.render_id
? managedImageUrl(String(row.render_id), row.render_updated_at)
: '',
iconUrl: iconUrl('specialization', row.id, row.icon_updated_at),
badges: Number(row.ready_focus_count)
? [{ label: 'BUILDER READY' }]
: [],
facts: [
{ label: 'FOCUS PATHS', value: String(row.focus_count) },
{ label: 'ABILITIES', value: String(row.ability_count) },
{ label: 'TREE NODES', value: String(row.node_count) },
],
lines: [
{ label: 'FOCUS PATHS', value: list(row.focus_names) },
{ label: 'ABILITIES', value: list(row.ability_names) },
],
description: row.description ? String(row.description) : undefined,
};
}
}) as DatabaseItem[];
const previewItems = items
.flatMap((item) => item.iconGroups ?? [])
.flatMap((group) => group.items)
.map((related) => related.preview)
.filter((preview): preview is DatabaseItem => Boolean(preview?.targetType));
const previewsByType = new Map<string, DatabaseItem[]>();
for (const preview of previewItems) {
const previews = previewsByType.get(preview.targetType!) ?? [];
previews.push(preview);
previewsByType.set(preview.targetType!, previews);
}
await Promise.all(
[...previewsByType.entries()].map(async ([type, previews]) => {
const uniqueIds = [...new Set(previews.map((preview) => preview.id))];
const summaries = await gameDataDiscussionSummary(
type as NonNullable<DatabaseItem['targetType']>,
uniqueIds,
);
for (const preview of previews) {
preview.pinnedCommunityNoteHtml =
summaries.get(preview.id)?.pinnedBodyHtml ?? undefined;
}
}),
);
const target = gameDataTarget(category.slug);
const discussionSummary = await gameDataDiscussionSummary(
target.type,
items.map((item) => item.id),
);
const selectedDiscussionId = url.searchParams.get('discussion')?.trim() ?? '';
const selectedItem =
items.find((item) => item.id === selectedDiscussionId) ?? null;
for (const item of items) {
const summary = discussionSummary.get(item.id);
item.targetType = target.type;
item.correctionHref = gameDataCorrectionHref(category.slug, item.id);
item.discussionCount = summary?.count ?? 0;
item.pinnedCommunityNoteHtml = summary?.pinnedBodyHtml ?? undefined;
item.discussionHref = `/database/${category.slug}?${new URLSearchParams({
...Object.fromEntries(url.searchParams),
discussion: item.id,
}).toString()}#discussion`;
}
const viewer = await getSessionUser(cookies);
const permissions = viewer ? await getPermissions(viewer) : [];
let discussionComments: Array<Record<string, unknown>> = [];
if (selectedItem) {
const discussionId = gameDataDiscussionId(target.type, selectedItem.id);
const rawComments = await (
await database.prepare(`SELECT comments.id,comments.author_id,comments.parent_id,
comments.body_markdown,comments.status,comments.created_at,comments.removal_reason,thread.pinned_comment_id,
COALESCE(profile.display_name,users.username,'Deleted Agent') AS display_name,profile.public_slug,
avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at
FROM comment_threads thread JOIN comments ON comments.thread_id=thread.id
LEFT JOIN users ON users.id=comments.author_id LEFT JOIN user_profiles profile ON profile.user_id=users.id
LEFT JOIN user_avatars avatar ON avatar.user_id=users.id
WHERE thread.target_type='game_data' AND thread.target_id=? AND comments.status IN ('visible','removed')
ORDER BY CASE WHEN comments.id=thread.pinned_comment_id THEN 0 ELSE 1 END,
comments.created_at DESC LIMIT 100`)
).all(discussionId);
const orderedComments = orderProfileCommentThread(
rawComments.map((comment) => {
const status =
String(comment.status) === 'removed'
? ('removed' as const)
: ('visible' as const);
return {
id: String(comment.id),
parentId: comment.parent_id ? String(comment.parent_id) : null,
status,
createdAt: String(comment.created_at),
bodyHtml:
status === 'visible'
? renderUserMarkdown(comment.body_markdown)
: '',
isPinned:
String(comment.id) === String(comment.pinned_comment_id ?? ''),
removalReason: comment.removal_reason
? String(comment.removal_reason)
: null,
author: {
id: String(comment.author_id),
displayName: String(comment.display_name),
publicSlug: comment.public_slug
? String(comment.public_slug)
: null,
avatarUrl: avatarUrl(
comment.avatar_source,
comment.avatar_value,
String(comment.author_id),
comment.avatar_updated_at,
),
roles: [] as string[],
},
};
}),
);
const pinnedCommentId = rawComments.find(
(comment) => comment.pinned_comment_id,
)?.pinned_comment_id;
const threadedComments = prioritizeCommentConversation(
orderedComments,
pinnedCommentId ? String(pinnedCommentId) : null,
);
const rootIds = [
...new Set(threadedComments.map((comment) => comment.rootId)),
];
const followedRootIds =
viewer && rootIds.length
? new Set(
(
await (
await database.prepare(`SELECT root_comment_id FROM comment_thread_follows
WHERE user_id=? AND root_comment_id IN (${rootIds.map(() => '?').join(',')})`)
).all(viewer.id, ...rootIds)
).map((row) => String(row.root_comment_id)),
)
: new Set<string>();
discussionComments = threadedComments.map((comment) => ({
...comment,
isFollowing: followedRootIds.has(comment.rootId),
canReport: Boolean(viewer) && viewer?.id !== comment.author.id,
canDelete:
Boolean(viewer) &&
(viewer?.id === comment.author.id ||
permissions.includes('*') ||
permissions.includes('community.comments.moderate')),
}));
}
return {
category,
items,
pagination,
search,
filters,
selectedItem,
discussionComments,
viewer: {
id: viewer?.id ?? null,
canComment: Boolean(viewer),
canPin:
permissions.includes('*') ||
permissions.includes('community.comments.moderate'),
},
};
};
export const actions: Actions = {
commentDiscussion: async ({ params, cookies, request }) => {
const commenter = await getSessionUser(cookies);
const form = await request.formData();
const parentId = String(form.get('parentId') ?? '').trim() || null;
const failure = (status: number, discussionError: string) =>
fail(status, {
discussionError,
discussionParentId: parentId,
discussionResultToken: randomUUID(),
});
if (!commenter) return failure(401, 'Sign in to add a community note.');
const category = databaseCategory(params.category);
if (!category) return failure(404, 'Database category not found.');
const target = gameDataTarget(category.slug);
const targetId = String(form.get('itemId') ?? '').trim();
if (!targetId || !(await gameDataTargetExists(target.type, targetId)))
return failure(404, 'That active data record is no longer available.');
const body = normalizeUserMarkdown(form.get('body'), 2_000);
if (body === null || Array.from(body).length < 2)
return failure(400, 'Notes must contain 2–2,000 visible characters.');
const database = await getDatabase();
const discussionId = gameDataDiscussionId(target.type, targetId);
const now = new Date().toISOString();
await (
await database.prepare(
`INSERT OR IGNORE INTO comment_threads(id,target_type,target_id,created_at) VALUES(?,'game_data',?,?)`,
)
).run(randomUUID(), discussionId, now);
const thread = await (
await database.prepare(
"SELECT id FROM comment_threads WHERE target_type='game_data' AND target_id=?",
)
).get(discussionId);
if (!thread?.id) return failure(500, 'The discussion could not be opened.');
if (parentId) {
const parent = await (
await database.prepare(
`SELECT id FROM comments WHERE id=? AND thread_id=? AND status='visible'`,
)
).get(parentId, thread.id);
if (!parent)
return failure(404, 'That note is no longer available for replies.');
}
const policy = await getCommunityModerationPolicy();
const moderation = await moderateCommunityText({
contentType: 'comment',
text: body,
policy,
});
const commentId = randomUUID();
const status =
moderation.decision === 'approved' ? 'visible' : 'pending_review';
await (
await database.prepare(
`INSERT INTO comments(id,thread_id,author_id,parent_id,body_markdown,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)`,
)
).run(commentId, thread.id, commenter.id, parentId, body, status, now, now);
await recordCommunityModerationScan({
contentType: 'comment',
contentId: commentId,
authorId: commenter.id,
result: moderation,
});
if (status === 'visible' && parentId) {
const rows = await (
await database.prepare(`SELECT id,parent_id,status,created_at FROM comments
WHERE thread_id=? AND status IN ('visible','removed')`)
).all(thread.id);
const conversation = orderProfileCommentThread(
rows.map((row) => ({
id: String(row.id),
parentId: row.parent_id ? String(row.parent_id) : null,
status:
String(row.status) === 'removed'
? ('removed' as const)
: ('visible' as const),
createdAt: String(row.created_at),
})),
).find((comment) => comment.id === parentId);
if (conversation) {
const followers = await (
await database.prepare(`SELECT user_id FROM comment_thread_follows
WHERE root_comment_id=? AND user_id<>?`)
).all(conversation.rootId, commenter.id);
const link = `/database/${category.slug}?discussion=${encodeURIComponent(targetId)}#discussion-comment-${encodeURIComponent(commentId)}`;
await Promise.all(
followers.map((follower) =>
createNotification({
userId: String(follower.user_id),
category: 'comments',
source: 'community',
title: 'New database discussion reply',
body: `${commenter.displayName} replied in the discussion for this database record.`,
link,
actions: [{ label: 'VIEW REPLY', href: link }],
}),
),
);
}
}
return {
discussionSaved: true,
discussionReviewQueued: status === 'pending_review',
discussionParentId: parentId,
discussionResultToken: randomUUID(),
};
},
setPinnedCommunityNote: async ({ params, cookies, request }) => {
const operator = await getSessionUser(cookies);
if (!operator)
return fail(401, {
discussionError: 'Sign in to manage Community Notes.',
discussionResultToken: randomUUID(),
});
const permissions = await getPermissions(operator);
if (
!permissions.includes('*') &&
!permissions.includes('community.comments.moderate')
)
return fail(403, {
discussionError: 'Community moderation permission required.',
discussionResultToken: randomUUID(),
});
const category = databaseCategory(params.category);
if (!category)
return fail(404, {
discussionError: 'Database category not found.',
discussionResultToken: randomUUID(),
});
const form = await request.formData();
const targetId = String(form.get('itemId') ?? '').trim();
const commentId = String(form.get('commentId') ?? '').trim();
const intent = String(form.get('intent') ?? '');
if (!['pin', 'unpin'].includes(intent))
return fail(400, {
discussionError: 'Choose whether to pin or unpin this note.',
discussionResultToken: randomUUID(),
});
const target = gameDataTarget(category.slug);
const database = await getDatabase();
const discussionId = gameDataDiscussionId(target.type, targetId);
const thread = await (
await database.prepare(
"SELECT id,pinned_comment_id FROM comment_threads WHERE target_type='game_data' AND target_id=?",
)
).get(discussionId);
if (!thread)
return fail(404, {
discussionError: 'Discussion not found.',
discussionResultToken: randomUUID(),
});
if (intent === 'pin') {
const comment = await (
await database.prepare(
"SELECT id FROM comments WHERE id=? AND thread_id=? AND parent_id IS NULL AND status='visible'",
)
).get(commentId, thread.id);
if (!comment)
return fail(404, {
discussionError:
'Only a visible root message in this discussion can be pinned.',
discussionResultToken: randomUUID(),
});
}
await (
await database.prepare(
'UPDATE comment_threads SET pinned_comment_id=? WHERE id=?',
)
).run(intent === 'pin' ? commentId : null, thread.id);
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action:
intent === 'pin' ? 'community_note.pinned' : 'community_note.unpinned',
entityId: thread.id,
before: {
targetType: target.type,
targetId,
pinnedCommentId: thread.pinned_comment_id ?? null,
},
after: {
targetType: target.type,
targetId,
pinnedCommentId: intent === 'pin' ? commentId : null,
},
});
return {
discussionPinned: intent === 'pin',
discussionResultToken: randomUUID(),
};
},
toggleDiscussionFollow: async ({ params, cookies, request }) => {
const viewer = await getSessionUser(cookies);
const form = await request.formData();
const itemId = String(form.get('itemId') ?? '').trim();
const commentId = String(form.get('commentId') ?? '').trim();
const intent = String(form.get('intent') ?? '').trim();
const failure = (status: number, message: string) =>
fail(status, {
discussionError: message,
discussionResultToken: randomUUID(),
});
if (!viewer) return failure(401, 'Sign in to follow a conversation.');
if (!commentId || !['follow', 'unfollow'].includes(intent))
return failure(400, 'Choose a conversation and follow preference.');
const category = databaseCategory(params.category);
if (!category) return failure(404, 'Database category not found.');
const target = gameDataTarget(category.slug);
const database = await getDatabase();
const discussionId = gameDataDiscussionId(target.type, itemId);
const rows = await (
await database.prepare(`SELECT comments.id,comments.parent_id,comments.status,comments.created_at
FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id
WHERE thread.target_type='game_data' AND thread.target_id=? AND comments.status IN ('visible','removed')`)
).all(discussionId);
const conversation = orderProfileCommentThread(
rows.map((row) => ({
id: String(row.id),
parentId: row.parent_id ? String(row.parent_id) : null,
status:
String(row.status) === 'removed'
? ('removed' as const)
: ('visible' as const),
createdAt: String(row.created_at),
})),
).find((comment) => comment.id === commentId);
if (!conversation)
return failure(404, 'That conversation is no longer available.');
if (intent === 'follow') {
await (
await database.prepare(`INSERT OR IGNORE INTO comment_thread_follows(user_id,root_comment_id,created_at)
VALUES(?,?,?)`)
).run(viewer.id, conversation.rootId, new Date().toISOString());
} else {
await (
await database.prepare(
'DELETE FROM comment_thread_follows WHERE user_id=? AND root_comment_id=?',
)
).run(viewer.id, conversation.rootId);
}
return {
discussionFollowSaved: true,
discussionFollowing: intent === 'follow',
discussionResultToken: randomUUID(),
};
},
reportContent: async ({ params, cookies, request }) => {
const reporter = await getSessionUser(cookies);
const form = await request.formData();
const itemId = String(form.get('itemId') ?? '').trim();
const contentType = String(form.get('contentType') ?? '');
const contentId = String(form.get('contentId') ?? '').trim();
const reportCategory = String(form.get('category') ?? '');
const details = String(form.get('details') ?? '')
.normalize('NFKC')
.trim();
const failure = (status: number, message: string) =>
fail(status, {
userReportError: message,
userReportContentId: contentId,
userReportContentType: contentType,
userReportResultToken: randomUUID(),
});
if (!reporter)
return failure(401, 'Sign in to request an Oversight review.');
if (contentType !== 'comment')
return failure(400, 'Choose reportable content.');
if (
![
'spam',
'harassment',
'hate',
'sexual',
'violence',
'self_harm',
'privacy',
'other',
].includes(reportCategory)
)
return failure(
400,
'Choose the policy concern that best fits this report.',
);
if (
details.length > 500 ||
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(details)
)
return failure(
400,
'Report details may contain up to 500 visible characters.',
);
const category = databaseCategory(params.category);
if (!category) return failure(404, 'Database category not found.');
const target = gameDataTarget(category.slug);
const discussionId = gameDataDiscussionId(target.type, itemId);
const database = await getDatabase();
const comment = await (
await database.prepare(`SELECT comments.author_id FROM comments
JOIN comment_threads thread ON thread.id=comments.thread_id
WHERE comments.id=? AND comments.status='visible' AND thread.target_type='game_data' AND thread.target_id=?`)
).get(contentId, discussionId);
if (!comment) return failure(404, 'That comment is no longer available.');
if (String(comment.author_id) === reporter.id)
return failure(400, 'You cannot report your own comment.');
const report = await createUserContentReport({
contentType: 'comment',
contentId,
reporterId: reporter.id,
category: reportCategory as
| 'spam'
| 'harassment'
| 'hate'
| 'sexual'
| 'violence'
| 'self_harm'
| 'privacy'
| 'other',
details,
});
if (!report)
return failure(
409,
'You already have a pending report for this content.',
);
return {
userReportSaved: true,
userReportContentId: contentId,
userReportContentType: contentType,
userReportResultToken: randomUUID(),
};
},
deleteDiscussionComment: async ({ params, cookies, request }) => {
const viewer = await getSessionUser(cookies);
const form = await request.formData();
const itemId = String(form.get('itemId') ?? '').trim();
const commentId = String(form.get('commentId') ?? '').trim();
const failure = (status: number, message: string) =>
fail(status, {
discussionError: message,
discussionResultToken: randomUUID(),
});
if (!viewer) return failure(401, 'Sign in to remove a comment.');
const category = databaseCategory(params.category);
if (!category) return failure(404, 'Database category not found.');
const target = gameDataTarget(category.slug);
const discussionId = gameDataDiscussionId(target.type, itemId);
const database = await getDatabase();
const comment = await (
await database.prepare(`SELECT comments.id,comments.author_id,comments.body_markdown,comments.parent_id
FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id
WHERE comments.id=? AND comments.status='visible' AND thread.target_type='game_data' AND thread.target_id=?`)
).get(commentId, discussionId);
if (!comment) return failure(404, 'That comment is no longer available.');
const permissions = await getPermissions(viewer);
const isModerator =
permissions.includes('*') ||
permissions.includes('community.comments.moderate');
if (String(comment.author_id) !== viewer.id && !isModerator)
return failure(403, 'You can remove only your own comment.');
const result = await removePublicComment({
database,
commentId,
removedBy: viewer.id,
removalReason:
isModerator && String(comment.author_id) !== viewer.id
? 'Removed by Community Oversight'
: 'Removed by poster',
});
if (!result.changed)
return failure(409, 'That comment changed before it could be removed.');
if (isModerator && String(comment.author_id) !== viewer.id) {
await recordIsacAudit({
actorId: viewer.id,
category: 'community_moderation',
action: 'comment.removed',
entityId: commentId,
before: {
targetType: target.type,
targetId: itemId,
authorId: comment.author_id,
bodyMarkdown: comment.body_markdown,
parentId: comment.parent_id,
},
after: {
retainedPlaceholder: result.retainedPlaceholder,
removalReason: 'Removed by Community Oversight',
},
});
}
return { discussionDeleted: true, discussionResultToken: randomUUID() };
},
};