Spaces:
Sleeping
Sleeping
// server.js — one‑vote‑per‑IP edition | |
const express = require('express'); | |
const bodyParser = require('body-parser'); | |
const fs = require('fs'); | |
const path = require('path'); | |
const requestIp = require('request-ip'); // NEW | |
const crypto = require('crypto'); // we'll hash IPs before saving | |
const archiver = require('./leaderboard_archiver'); | |
const https = require('https'); // For Hugging Face API requests | |
const PORT = process.env.PORT || 3000; | |
const DATA_FILE = path.join(__dirname, 'data', 'data.json'); | |
const IP_FILE = path.join(__dirname, 'data', 'ips.json'); | |
const CATEGORIES = ["6gb", "12gb", "16gb", "24gb", "48gb", "72gb", "96gb"]; | |
function validateCategory(cat) { | |
return CATEGORIES.includes(cat); | |
} | |
const app = express(); | |
app.use(bodyParser.json()); | |
app.use(requestIp.mw()); // adds req.clientIp | |
app.use(express.static(path.join(__dirname, 'public'))); | |
/* ---------- tiny helpers ---------- */ | |
function readJson(file, fallback) { | |
try { return JSON.parse(fs.readFileSync(file)); } | |
catch { return fallback; } | |
} | |
function writeJson(file, obj) { | |
fs.writeFileSync(file, JSON.stringify(obj, null, 2)); | |
} | |
function hash(ip) { // do not store raw IP | |
return crypto.createHash('sha256').update(ip).digest('hex'); | |
} | |
/* ---------- IP‑limit middleware ---------- */ | |
function oneVotePerIP(req, res, next) { | |
const ipList = readJson(IP_FILE, {}); | |
const key = hash(req.clientIp || 'unknown'); | |
if (ipList[key]) return res.status(409) | |
.json({ error: 'You have already voted from this IP' }); | |
req._ipKey = key; // remember for later | |
next(); | |
} | |
/* ---------- Ensure IP tracking is properly formatted ---------- */ | |
function ensureValidIpTracking() { | |
const ips = readJson(IP_FILE, {}); | |
let changed = false; | |
// Convert any string values to objects | |
Object.keys(ips).forEach(key => { | |
if (typeof ips[key] === 'string') { | |
ips[key] = {}; | |
changed = true; | |
} | |
}); | |
if (changed) { | |
writeJson(IP_FILE, ips); | |
} | |
return ips; | |
} | |
/* ---------- API ---------- */ | |
app.get('/api/entries', (req, res) => { | |
const category = req.query.category; | |
const data = readJson(DATA_FILE, {}); | |
if (!validateCategory(category)) { | |
return res.status(400).json({ error: 'Invalid category' }); | |
} | |
const entries = (data[category] || []).sort((a, b) => b.votes - a.votes); | |
res.json(entries); | |
}); | |
/* Add new entry + cast initial vote */ | |
app.post('/api/add', (req, res) => { | |
const name = (req.body.name || '').trim(); | |
const category = req.body.category; | |
if (!name) return res.status(400).json({ error: 'Name required' }); | |
if (!validateCategory(category)) return res.status(400).json({ error: 'Invalid category' }); | |
const data = readJson(DATA_FILE, {}); | |
const list = data[category] = data[category] || []; | |
if (list.find(e => e.name.toLowerCase() === name.toLowerCase())) | |
return res.status(400).json({ error: 'Entry already exists' }); | |
const ips = ensureValidIpTracking(); | |
const ipKey = hash(req.clientIp || 'unknown'); | |
if (!ips[ipKey] || typeof ips[ipKey] !== 'object') ips[ipKey] = {}; | |
const prevVotedId = ips[ipKey][category]; | |
// If user has already voted for another entry, decrement its votes | |
if (prevVotedId) { | |
const prevItem = list.find(e => e.id === prevVotedId); | |
if (prevItem && prevItem.votes > 0) prevItem.votes -= 1; | |
} | |
// Add new entry with 1 vote | |
const entry = { id: Date.now().toString(), name, votes: 1 }; | |
list.push(entry); | |
writeJson(DATA_FILE, data); | |
// Update IP record to new entry id for this category | |
ips[ipKey][category] = entry.id; | |
writeJson(IP_FILE, ips); | |
res.json(entry); | |
}); | |
/* Vote for existing entry */ | |
app.post('/api/vote', (req, res) => { | |
const { id, category } = req.body; | |
if (!validateCategory(category)) return res.status(400).json({ error: 'Invalid category' }); | |
const data = readJson(DATA_FILE, {}); | |
const list = data[category] = data[category] || []; | |
const item = list.find(e => e.id === id); | |
if (!item) return res.status(404).json({ error: 'Entry not found' }); | |
const ips = ensureValidIpTracking(); | |
const ipKey = hash(req.clientIp || 'unknown'); | |
if (!ips[ipKey] || typeof ips[ipKey] !== 'object') ips[ipKey] = {}; | |
const prevVotedId = ips[ipKey][category]; | |
if (prevVotedId === id) { | |
// Already voted for this option | |
return res.status(409).json({ error: 'You have already voted for this option' }); | |
} | |
// If user has voted for a different option, decrement that vote | |
if (prevVotedId) { | |
const prevItem = list.find(e => e.id === prevVotedId); | |
if (prevItem && prevItem.votes > 0) prevItem.votes -= 1; | |
} | |
// Increment vote for the new option | |
item.votes += 1; | |
writeJson(DATA_FILE, data); | |
// Update IP record to new voted id for this category | |
ips[ipKey][category] = id; | |
writeJson(IP_FILE, ips); | |
res.json(item); | |
}); | |
/* ---------- Archive API ---------- */ | |
// Get list of archived weeks | |
app.get('/api/archives/weeks', (req, res) => { | |
try { | |
const weeks = archiver.getArchivedWeeks(); | |
res.json(weeks); | |
} catch (error) { | |
console.error('Error getting archived weeks:', error); | |
res.status(500).json({ error: 'Failed to retrieve archived weeks' }); | |
} | |
}); | |
// Get archived data for a specific week | |
app.get('/api/archives/week/:weekId', (req, res) => { | |
try { | |
const { weekId } = req.params; | |
const archive = archiver.getArchivedWeek(weekId); | |
if (!archive) { | |
return res.status(404).json({ error: 'Archive not found for the specified week' }); | |
} | |
res.json(archive); | |
} catch (error) { | |
console.error('Error getting archived week:', error); | |
res.status(500).json({ error: 'Failed to retrieve archived data' }); | |
} | |
}); | |
// Get archived data for a specific week and category | |
app.get('/api/archives/week/:weekId/category/:category', (req, res) => { | |
try { | |
const { weekId, category } = req.params; | |
const archive = archiver.getArchivedWeek(weekId); | |
if (!archive) { | |
return res.status(404).json({ error: 'Archive not found for the specified week' }); | |
} | |
if (!validateCategory(category)) { | |
return res.status(400).json({ error: 'Invalid category' }); | |
} | |
const entries = (archive.data[category] || []).sort((a, b) => b.votes - a.votes); | |
res.json(entries); | |
} catch (error) { | |
console.error('Error getting archived category:', error); | |
res.status(500).json({ error: 'Failed to retrieve archived data' }); | |
} | |
}); | |
// Get archived data for a date range | |
app.get('/api/archives/range', (req, res) => { | |
try { | |
const { startDate, endDate } = req.query; | |
if (!startDate || !endDate) { | |
return res.status(400).json({ error: 'Both startDate and endDate are required' }); | |
} | |
const archives = archiver.getArchivedRange(startDate, endDate); | |
res.json(archives); | |
} catch (error) { | |
console.error('Error getting archived range:', error); | |
res.status(500).json({ error: 'Failed to retrieve archived data for the specified range' }); | |
} | |
}); | |
/* ---------- Hugging Face API Proxy ---------- */ | |
app.get('/api/huggingface/models', (req, res) => { | |
const query = req.query.query; | |
if (!query || query.length < 2) { | |
return res.status(400).json({ error: 'Query must be at least 2 characters' }); | |
} | |
const options = { | |
hostname: 'huggingface.co', | |
path: `/api/models?search=${encodeURIComponent(query)}`, | |
method: 'GET', | |
headers: { | |
'Accept': 'application/json' | |
} | |
}; | |
const hfRequest = https.request(options, (hfResponse) => { | |
let data = ''; | |
hfResponse.on('data', (chunk) => { | |
data += chunk; | |
}); | |
hfResponse.on('end', () => { | |
try { | |
const parsedData = JSON.parse(data); | |
// Format the response to include only necessary information | |
const formattedResults = parsedData.map(model => ({ | |
id: model.id, | |
modelId: model.modelId, | |
name: model.name || model.id, | |
author: model.author?.name || 'Unknown', | |
downloads: model.downloads || 0, | |
likes: model.likes || 0 | |
})).slice(0, 10); // Limit to 10 results | |
res.json(formattedResults); | |
} catch (error) { | |
console.error('Error parsing Hugging Face API response:', error); | |
res.status(500).json({ error: 'Failed to parse Hugging Face API response' }); | |
} | |
}); | |
}); | |
hfRequest.on('error', (error) => { | |
console.error('Error fetching from Hugging Face API:', error); | |
res.status(500).json({ error: 'Failed to fetch from Hugging Face API' }); | |
}); | |
hfRequest.end(); | |
}); | |
/* ---------- start ---------- */ | |
app.listen(PORT, () => console.log('Leaderboard running on', PORT)); |