Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions apiTools/wipeDetector.js
Original file line number Diff line number Diff line change
@@ -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 };
21 changes: 21 additions & 0 deletions models/WipeDetection.js
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions railway.toml
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions routes/Wipes.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'));

Expand Down
14 changes: 13 additions & 1 deletion structures/Pit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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=>{
Expand Down