From caf6f14c22e56da2904a0ef55c9c87c678a0af75 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 16:10:02 +0000 Subject: [PATCH] Add wipe detection system, public /api/wipes endpoint, and Railway.app config - WipeDetection model stores wipe events (before/after prestige, playtime, xp) - wipeDetector compares old DB doc to fresh Hypixel data on every player save; fires Discord webhook when a prestige/stat reset is detected - Pit.js fetches old doc before upsert so wipe comparison has baseline - GET /api/wipes lists all detected wipes (public, paginated) - GET /api/wipes/:uuid lists wipe history for a specific player - railway.toml wires up nixpacks build with canvas native deps + node start - npm start now runs node directly (nodemon moved to npm run dev) https://claude.ai/code/session_01GGsxUq8CVif1mu81xihd9j --- apiTools/wipeDetector.js | 88 ++++++++++++++++++++++++++++++++++++++++ models/WipeDetection.js | 21 ++++++++++ package.json | 3 +- railway.toml | 22 ++++++++++ routes/Wipes.js | 34 ++++++++++++++++ routes/index.js | 2 + structures/Pit.js | 14 ++++++- 7 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 apiTools/wipeDetector.js create mode 100644 models/WipeDetection.js create mode 100644 railway.toml create mode 100644 routes/Wipes.js diff --git a/apiTools/wipeDetector.js b/apiTools/wipeDetector.js new file mode 100644 index 0000000..a10c12b --- /dev/null +++ b/apiTools/wipeDetector.js @@ -0,0 +1,88 @@ +const fetch = require('node-fetch'); +const WipeDetection = require('../models/WipeDetection'); + +const WEBHOOK_URL = 'https://discord.com/api/webhooks/1502702864219373578/RBJ3n1SZwBaZJQHHeEywfDUN7cVPi-8yy8WgJkuIsfe0kuYv8E8ASItoDiLRxs_S-po8'; + +// Wipe: player had meaningful progress and it all reset to zero +const isWipe = (oldPrestige, oldPlaytime, newPrestige, newPlaytime, newXp) => { + const hadProgress = oldPrestige > 0 || oldPlaytime > 120; + const totalReset = newPrestige === 0 && newPlaytime === 0 && newXp === 0; + const prestigeReset = oldPrestige > 1 && newPrestige === 0; + return hadProgress && (totalReset || prestigeReset); +}; + +const sendWipeWebhook = (wipeDoc) => { + const embed = { + title: 'Wipe Detected', + color: 0xff4444, + fields: [ + { name: 'Player', value: wipeDoc.name || wipeDoc.uuid, inline: true }, + { name: 'UUID', value: `\`${wipeDoc.uuid}\``, inline: true }, + { name: '​', value: '​', inline: true }, + { + name: 'Before', + value: `Prestige: **${wipeDoc.before.prestige}**\nPlaytime: **${wipeDoc.before.playtime}m**\nXP: **${wipeDoc.before.xp}**`, + inline: true, + }, + { + name: 'After', + value: `Prestige: **${wipeDoc.after.prestige}**\nPlaytime: **${wipeDoc.after.playtime}m**\nXP: **${wipeDoc.after.xp}**`, + inline: true, + }, + ], + timestamp: new Date().toISOString(), + footer: { text: 'PitPanda Wipe Detection' }, + }; + fetch(WEBHOOK_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ embeds: [embed] }), + }).catch(err => console.error('Wipe webhook failed:', err)); +}; + +/** + * Called after a player doc is saved. Compares old DB doc with fresh Hypixel data. + * @param {string} uuid + * @param {string} name + * @param {object|null} oldDoc - previous player document (lean) + * @param {number} newPrestige + * @param {number} newPlaytime + * @param {number} newXp + * @param {number} newLifetimeGold + */ +const checkForWipe = async (uuid, name, oldDoc, newPrestige, newPlaytime, newXp, newLifetimeGold) => { + if (!oldDoc) return; + + const oldPrestige = (oldDoc.prestigeTimes || []).length; + const oldPlaytime = oldDoc.playtime || 0; + + if (!isWipe(oldPrestige, oldPlaytime, newPrestige, newPlaytime, newXp)) return; + + // Avoid duplicate wipe entries within 1 hour + const recent = await WipeDetection.findOne({ + uuid, + detectedAt: { $gte: new Date(Date.now() - 3600e3) }, + }).lean(); + if (recent) return; + + const wipeDoc = await WipeDetection.create({ + uuid, + name, + before: { + prestige: oldPrestige, + playtime: oldPlaytime, + xp: oldDoc.xp || 0, + lifetimeGold: oldDoc.lifetimeGold || 0, + }, + after: { + prestige: newPrestige, + playtime: newPlaytime, + xp: newXp, + lifetimeGold: newLifetimeGold, + }, + }).catch(err => { console.error('Failed to save wipe detection:', err); return null; }); + + if (wipeDoc) sendWipeWebhook(wipeDoc); +}; + +module.exports = { checkForWipe }; diff --git a/models/WipeDetection.js b/models/WipeDetection.js new file mode 100644 index 0000000..8bef652 --- /dev/null +++ b/models/WipeDetection.js @@ -0,0 +1,21 @@ +const mongoose = require('mongoose'); + +const WipeDetectionSchema = mongoose.Schema({ + uuid: { type: String, required: true, index: true }, + name: String, + detectedAt: { type: Date, default: Date.now, index: true }, + before: { + prestige: Number, + playtime: Number, + xp: Number, + lifetimeGold: Number, + }, + after: { + prestige: Number, + playtime: Number, + xp: Number, + lifetimeGold: Number, + }, +}); + +module.exports = mongoose.model('WipeDetections', WipeDetectionSchema); diff --git a/package.json b/package.json index 65bdfc9..b1fde4c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "start": "nodemon index", + "start": "node index.js", + "dev": "nodemon index", "addkey": "node microTools/addApiKey.js", "indexer": "node indexer/index.js", "imageapi": "node imageApi/index.js", diff --git a/railway.toml b/railway.toml new file mode 100644 index 0000000..b4ad229 --- /dev/null +++ b/railway.toml @@ -0,0 +1,22 @@ +[build] +builder = "nixpacks" + +[build.nixpacksPlan.phases.setup] +nixPkgs = [ + "cairo", + "pango", + "libjpeg", + "giflib", + "librsvg", + "pkg-config", + "python3", + "gcc", + "gnumake", +] + +[deploy] +startCommand = "node index.js" +healthcheckPath = "/api/indexer" +healthcheckTimeout = 30 +restartPolicyType = "on_failure" +restartPolicyMaxRetries = 3 diff --git a/routes/Wipes.js b/routes/Wipes.js new file mode 100644 index 0000000..c130f17 --- /dev/null +++ b/routes/Wipes.js @@ -0,0 +1,34 @@ +const router = require('express').Router(); +const WipeDetection = require('../models/WipeDetection'); + +// GET /api/wipes - list all detected wipes, newest first +router.get('/', async (req, res) => { + try { + const limit = Math.min(parseInt(req.query.limit) || 50, 100); + const page = Math.max(parseInt(req.query.page) || 1, 1); + const skip = (page - 1) * limit; + + const [wipes, total] = await Promise.all([ + WipeDetection.find().sort({ detectedAt: -1 }).skip(skip).limit(limit).lean(), + WipeDetection.countDocuments(), + ]); + + res.json({ success: true, data: { wipes, total, page, limit } }); + } catch (err) { + res.status(500).json({ success: false, error: 'Internal server error' }); + } +}); + +// GET /api/wipes/:uuid - wipe history for a specific player +router.get('/:uuid', async (req, res) => { + try { + const wipes = await WipeDetection.find({ uuid: req.params.uuid }) + .sort({ detectedAt: -1 }) + .lean(); + res.json({ success: true, data: wipes }); + } catch (err) { + res.status(500).json({ success: false, error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js index dad5a21..25940e9 100644 --- a/routes/index.js +++ b/routes/index.js @@ -21,6 +21,7 @@ const add = require('./Add'); const friends = require('./Friends'); const keyGen = require('./KeyGen'); const keyInfo = require('./KeyInfo'); +const wipes = require('./Wipes'); let statBatch = {}; const batchSize = 10; @@ -61,6 +62,7 @@ router.use('/add', add); router.use('/friends', friends); router.use('/keygen', keyGen); router.use('/keyinfo', keyInfo); +router.use('/wipes', wipes); router.use('*', APIerror('Invalid Endpoint')); diff --git a/structures/Pit.js b/structures/Pit.js index 7d4c1f8..e3190cd 100644 --- a/structures/Pit.js +++ b/structures/Pit.js @@ -22,6 +22,7 @@ const [ renownShopSize, renownShopTotalCost ] = Object.values(RenownUpgrades).re const textHelpers = require('../utils/TextHelpers'); const { logMystics } = require('../apiTools/mysticLogging'); +const { checkForWipe } = require('../apiTools/wipeDetector'); function removeFromLB(uuid){ Object.keys(Leaderboards) @@ -1227,7 +1228,18 @@ class Pit { this.playerDoc; Object.defineProperty(this,'playerDoc',{ enumerable: false, - value: new Promise(resolve=>Player.findByIdAndUpdate(this.uuid, { $set: this.createPlayerDoc(), $inc: {searches: 1} }, { upsert: true, new: true }).then(resolve)) + value: (async () => { + const oldDoc = await Player.findById(this.uuid, { + prestigeTimes: 1, playtime: 1, xp: 1, lifetimeGold: 1, + }).lean(); + const newDoc = await Player.findByIdAndUpdate( + this.uuid, + { $set: this.createPlayerDoc(), $inc: { searches: 1 } }, + { upsert: true, new: true } + ); + checkForWipe(this.uuid, this.name, oldDoc, this.prestige, this.playtime, this.xp, this.lifetimeGold); + return newDoc; + })() }); this.playerDoc.then(doc=>{