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 => { try { const parsed = JSON.parse(String(value ?? '{}')); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record) : {}; } catch { return {}; } }; const talentCardItem = ( row: Record, mechanics: Array>, 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(); 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, 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> = []; 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(); 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() }; }, };