Peacemanguy/LLMChoice
0
1// server.js — one‑vote‑per‑IP edition2const express = require('express');3const bodyParser = require('body-parser');4const fs = require('fs');5const path = require('path');6const requestIp = require('request-ip'); // NEW7const crypto = require('crypto'); // we'll hash IPs before saving8const archiver = require('./leaderboard_archiver');9const https = require('https'); // For Hugging Face API requests10 11const PORT = process.env.PORT || 3000;12const DATA_FILE = path.join(__dirname, 'data', 'data.json');13const IP_FILE = path.join(__dirname, 'data', 'ips.json');14 15const CATEGORIES = ["6gb", "12gb", "16gb", "24gb", "48gb", "72gb", "96gb"];16function validateCategory(cat) {17 return CATEGORIES.includes(cat);18}19 20const app = express();21app.use(bodyParser.json());22app.use(requestIp.mw()); // adds req.clientIp23app.use(express.static(path.join(__dirname, 'public')));24 25/* ---------- tiny helpers ---------- */26function readJson(file, fallback) {27 try { return JSON.parse(fs.readFileSync(file)); }28 catch { return fallback; }29}30function writeJson(file, obj) {31 fs.writeFileSync(file, JSON.stringify(obj, null, 2));32}33function hash(ip) { // do not store raw IP34 return crypto.createHash('sha256').update(ip).digest('hex');35}36 37/* ---------- IP‑limit middleware ---------- */38function oneVotePerIP(req, res, next) {39 const ipList = readJson(IP_FILE, {});40 const key = hash(req.clientIp || 'unknown');41 if (ipList[key]) return res.status(409)42 .json({ error: 'You have already voted from this IP' });43 req._ipKey = key; // remember for later44 next();45}46 47/* ---------- Ensure IP tracking is properly formatted ---------- */48function ensureValidIpTracking() {49 const ips = readJson(IP_FILE, {});50 let changed = false;51 52 // Convert any string values to objects53 Object.keys(ips).forEach(key => {54 if (typeof ips[key] === 'string') {55 ips[key] = {};56 changed = true;57 }58 });59 60 if (changed) {61 writeJson(IP_FILE, ips);62 }63 64 return ips;65}66 67/* ---------- API ---------- */68app.get('/api/entries', (req, res) => {69 const category = req.query.category;70 const data = readJson(DATA_FILE, {});71 if (!validateCategory(category)) {72 return res.status(400).json({ error: 'Invalid category' });73 }74 const entries = (data[category] || []).sort((a, b) => b.votes - a.votes);75 res.json(entries);76});77 78/* Add new entry + cast initial vote */79app.post('/api/add', (req, res) => {80 const name = (req.body.name || '').trim();81 const category = req.body.category;82 if (!name) return res.status(400).json({ error: 'Name required' });83 if (!validateCategory(category)) return res.status(400).json({ error: 'Invalid category' });84 85 const data = readJson(DATA_FILE, {});86 const list = data[category] = data[category] || [];87 if (list.find(e => e.name.toLowerCase() === name.toLowerCase()))88 return res.status(400).json({ error: 'Entry already exists' });89 90 const ips = ensureValidIpTracking();91 const ipKey = hash(req.clientIp || 'unknown');92 if (!ips[ipKey] || typeof ips[ipKey] !== 'object') ips[ipKey] = {};93 const prevVotedId = ips[ipKey][category];94 95 // If user has already voted for another entry, decrement its votes96 if (prevVotedId) {97 const prevItem = list.find(e => e.id === prevVotedId);98 if (prevItem && prevItem.votes > 0) prevItem.votes -= 1;99 }100 101 // Add new entry with 1 vote102 const entry = { id: Date.now().toString(), name, votes: 1 };103 list.push(entry);104 writeJson(DATA_FILE, data);105 106 // Update IP record to new entry id for this category107 ips[ipKey][category] = entry.id;108 writeJson(IP_FILE, ips);109 110 res.json(entry);111});112 113/* Vote for existing entry */114app.post('/api/vote', (req, res) => {115 const { id, category } = req.body;116 if (!validateCategory(category)) return res.status(400).json({ error: 'Invalid category' });117 118 const data = readJson(DATA_FILE, {});119 const list = data[category] = data[category] || [];120 const item = list.find(e => e.id === id);121 if (!item) return res.status(404).json({ error: 'Entry not found' });122 123 const ips = ensureValidIpTracking();124 const ipKey = hash(req.clientIp || 'unknown');125 if (!ips[ipKey] || typeof ips[ipKey] !== 'object') ips[ipKey] = {};126 const prevVotedId = ips[ipKey][category];127 128 if (prevVotedId === id) {129 // Already voted for this option130 return res.status(409).json({ error: 'You have already voted for this option' });131 }132 133 // If user has voted for a different option, decrement that vote134 if (prevVotedId) {135 const prevItem = list.find(e => e.id === prevVotedId);136 if (prevItem && prevItem.votes > 0) prevItem.votes -= 1;137 }138 139 // Increment vote for the new option140 item.votes += 1;141 writeJson(DATA_FILE, data);142 143 // Update IP record to new voted id for this category144 ips[ipKey][category] = id;145 writeJson(IP_FILE, ips);146 147 res.json(item);148});149 150/* ---------- Archive API ---------- */151// Get list of archived weeks152app.get('/api/archives/weeks', (req, res) => {153 try {154 const weeks = archiver.getArchivedWeeks();155 res.json(weeks);156 } catch (error) {157 console.error('Error getting archived weeks:', error);158 res.status(500).json({ error: 'Failed to retrieve archived weeks' });159 }160});161 162// Get archived data for a specific week163app.get('/api/archives/week/:weekId', (req, res) => {164 try {165 const { weekId } = req.params;166 const archive = archiver.getArchivedWeek(weekId);167 168 if (!archive) {169 return res.status(404).json({ error: 'Archive not found for the specified week' });170 }171 172 res.json(archive);173 } catch (error) {174 console.error('Error getting archived week:', error);175 res.status(500).json({ error: 'Failed to retrieve archived data' });176 }177});178 179// Get archived data for a specific week and category180app.get('/api/archives/week/:weekId/category/:category', (req, res) => {181 try {182 const { weekId, category } = req.params;183 const archive = archiver.getArchivedWeek(weekId);184 185 if (!archive) {186 return res.status(404).json({ error: 'Archive not found for the specified week' });187 }188 189 if (!validateCategory(category)) {190 return res.status(400).json({ error: 'Invalid category' });191 }192 193 const entries = (archive.data[category] || []).sort((a, b) => b.votes - a.votes);194 res.json(entries);195 } catch (error) {196 console.error('Error getting archived category:', error);197 res.status(500).json({ error: 'Failed to retrieve archived data' });198 }199});200 201// Get archived data for a date range202app.get('/api/archives/range', (req, res) => {203 try {204 const { startDate, endDate } = req.query;205 206 if (!startDate || !endDate) {207 return res.status(400).json({ error: 'Both startDate and endDate are required' });208 }209 210 const archives = archiver.getArchivedRange(startDate, endDate);211 res.json(archives);212 } catch (error) {213 console.error('Error getting archived range:', error);214 res.status(500).json({ error: 'Failed to retrieve archived data for the specified range' });215 }216});217 218/* ---------- Hugging Face API Proxy ---------- */219app.get('/api/huggingface/models', (req, res) => {220 const query = req.query.query;221 222 if (!query || query.length < 2) {223 return res.status(400).json({ error: 'Query must be at least 2 characters' });224 }225 226 const options = {227 hostname: 'huggingface.co',228 path: `/api/models?search=${encodeURIComponent(query)}`,229 method: 'GET',230 headers: {231 'Accept': 'application/json'232 }233 };234 235 const hfRequest = https.request(options, (hfResponse) => {236 let data = '';237 238 hfResponse.on('data', (chunk) => {239 data += chunk;240 });241 242 hfResponse.on('end', () => {243 try {244 const parsedData = JSON.parse(data);245 246 // Format the response to include only necessary information247 const formattedResults = parsedData.map(model => ({248 id: model.id,249 modelId: model.modelId,250 name: model.name || model.id,251 author: model.author?.name || 'Unknown',252 downloads: model.downloads || 0,253 likes: model.likes || 0254 })).slice(0, 10); // Limit to 10 results255 256 res.json(formattedResults);257 } catch (error) {258 console.error('Error parsing Hugging Face API response:', error);259 res.status(500).json({ error: 'Failed to parse Hugging Face API response' });260 }261 });262 });263 264 hfRequest.on('error', (error) => {265 console.error('Error fetching from Hugging Face API:', error);266 res.status(500).json({ error: 'Failed to fetch from Hugging Face API' });267 });268 269 hfRequest.end();270});271 272/* ---------- start ---------- */273app.listen(PORT, () => console.log('Leaderboard running on', PORT));