Initial commit
This commit is contained in:
commit
08b0f6d36c
125
.gitignore
vendored
Normal file
125
.gitignore
vendored
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
# ---> Node
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
*.sql*
|
||||||
|
players.msgpack
|
||||||
|
.env
|
||||||
|
ddnss/build
|
26
api/api.js
Normal file
26
api/api.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Router } from 'express'
|
||||||
|
import playerApi from './players.js'
|
||||||
|
import mapApi from './maps.js'
|
||||||
|
import finishesApi from './finishes.js'
|
||||||
|
import graphApi from './graph.js'
|
||||||
|
|
||||||
|
const api = Router()
|
||||||
|
|
||||||
|
api.get(
|
||||||
|
'/',
|
||||||
|
(req, res) => res.json({
|
||||||
|
success: true,
|
||||||
|
response: "You connected to DDStats API! :D"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
api.use('/players', playerApi)
|
||||||
|
api.use('/maps', mapApi)
|
||||||
|
api.use('/finishes', finishesApi)
|
||||||
|
api.use('/graph', graphApi)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module is the entrypoint for the API.
|
||||||
|
* @module api/api
|
||||||
|
*/
|
||||||
|
export default api
|
19
api/finishes.js
Normal file
19
api/finishes.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Router } from 'express'
|
||||||
|
import { sqlite } from '../db/init.js'
|
||||||
|
|
||||||
|
const finishApi = Router()
|
||||||
|
|
||||||
|
/* TODO: precalculate this */
|
||||||
|
finishApi.get(
|
||||||
|
'/count',
|
||||||
|
(req, res) => {
|
||||||
|
const finishes = sqlite.prepare(`SELECT COUNT(*) as count FROM race`).get()
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: finishes.count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default finishApi
|
46
api/graph.js
Normal file
46
api/graph.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { Router } from 'express'
|
||||||
|
import { sqlite } from '../db/init.js'
|
||||||
|
|
||||||
|
const graphApi = Router()
|
||||||
|
|
||||||
|
/* TODO: precalculate this */
|
||||||
|
graphApi.get(
|
||||||
|
'/points',
|
||||||
|
(req, res) => {
|
||||||
|
/* Check if a query was provided */
|
||||||
|
if (!req.query.q) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
response: "No query ('host/path?q=query') provided!"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let player = req.query.q
|
||||||
|
|
||||||
|
const finishes = sqlite.prepare(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT(a.map), a.timestamp, b.points
|
||||||
|
FROM race AS a
|
||||||
|
INNER JOIN maps AS b
|
||||||
|
ON a.map = b.map
|
||||||
|
WHERE a.NAME = ?
|
||||||
|
AND a.map LIKE '%'
|
||||||
|
GROUP BY a.map
|
||||||
|
ORDER BY a.timestamp;
|
||||||
|
`)
|
||||||
|
|
||||||
|
let currentPoints = 0
|
||||||
|
let array = []
|
||||||
|
for (const finish of finishes.iterate(player)) {
|
||||||
|
console.log(finish)
|
||||||
|
currentPoints += finish.Points
|
||||||
|
array.push({ t: new Date(finish.Timestamp), y: currentPoints })
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: array,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default graphApi
|
175
api/maps.js
Normal file
175
api/maps.js
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import { Router } from 'express'
|
||||||
|
import { sqlite } from '../db/init.js'
|
||||||
|
|
||||||
|
const mapApi = Router()
|
||||||
|
|
||||||
|
mapApi.get(
|
||||||
|
'/count',
|
||||||
|
(req, res) => {
|
||||||
|
const totalMaps = sqlite.prepare(`SELECT COUNT(*) as amount FROM maps`).get()
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: totalMaps.amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
mapApi.get(
|
||||||
|
'/get/:map',
|
||||||
|
(req, res) => {
|
||||||
|
let map = req.params.map
|
||||||
|
|
||||||
|
/* Check if map exists */
|
||||||
|
const check = sqlite.prepare(`SELECT map FROM maps WHERE map = ?`).get(map)
|
||||||
|
if (!check) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
response: "No map found!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = sqlite.prepare(`SELECT * FROM maps WHERE map = ?`).get(map)
|
||||||
|
|
||||||
|
/* TODO: Generate a table with this as a cache */
|
||||||
|
/* Also generate indexes */
|
||||||
|
const avgTime = sqlite.prepare(`SELECT avg(Time) as 'averageTime' FROM race WHERE map = ?`).get(map)
|
||||||
|
const total = sqlite.prepare(`SELECT COUNT(*) as 'total' FROM race WHERE map = ?`).get(map)
|
||||||
|
const unique = sqlite.prepare(`SELECT COUNT(distinct(name)) as 'unique' FROM race WHERE map = ?`).get(map)
|
||||||
|
const teams = sqlite.prepare(`SELECT COUNT(distinct(ID)) as 'teams' FROM teamrace WHERE map = ?`).get(map)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: {
|
||||||
|
name: info.Map,
|
||||||
|
category: info.Server,
|
||||||
|
awardPoints: info.Points,
|
||||||
|
rating: info.Stars,
|
||||||
|
mapper: info.Mapper,
|
||||||
|
release: info.Timestamp,
|
||||||
|
/* TODO Get median time*/
|
||||||
|
averageTime: avgTime.averageTime,
|
||||||
|
finishes: {
|
||||||
|
unique: unique.unique,
|
||||||
|
total: total.total,
|
||||||
|
teams: teams.teams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
mapApi.get(
|
||||||
|
'/getAll',
|
||||||
|
(req, res) => {
|
||||||
|
|
||||||
|
const allMaps = sqlite.prepare(
|
||||||
|
`SELECT
|
||||||
|
map as name,
|
||||||
|
server as category,
|
||||||
|
points as awardPoints,
|
||||||
|
stars as rating,
|
||||||
|
mapper as mapper,
|
||||||
|
timestamp as release
|
||||||
|
FROM maps`).all()
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: allMaps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
mapApi.get(
|
||||||
|
'/category/:category',
|
||||||
|
(req, res) => {
|
||||||
|
let category = req.params.category
|
||||||
|
|
||||||
|
/* Check if category exists */
|
||||||
|
const check = sqlite.prepare(`SELECT server FROM maps WHERE server = ? LIMIT 1`).get(category)
|
||||||
|
if (!check) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
response: "Invalid category name!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const allMaps = sqlite.prepare(
|
||||||
|
`SELECT
|
||||||
|
map as name,
|
||||||
|
server as category,
|
||||||
|
points as awardPoints,
|
||||||
|
stars as rating,
|
||||||
|
mapper as mapper,
|
||||||
|
timestamp as release
|
||||||
|
FROM maps WHERE server = ?`).all(category)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: allMaps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Searching allows you to attach sql LIKE %
|
||||||
|
* Example to search for maps beginning with Kobra
|
||||||
|
* You can do search for Kobra%
|
||||||
|
|
||||||
|
* This thing is a complete mess, send help. BUT it does work <3 */
|
||||||
|
mapApi.get(
|
||||||
|
'/search',
|
||||||
|
async (req, res) => {
|
||||||
|
/* Check if a query was provided */
|
||||||
|
if (!req.query.q || !req.query.byMapper) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
response: "No query ('host/path?q=query') provided!"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = req.query.q
|
||||||
|
|
||||||
|
/* Set defaults */
|
||||||
|
let limit = 20
|
||||||
|
let offset = 0
|
||||||
|
let sortBy = "timestamp"
|
||||||
|
let order = "asc"
|
||||||
|
let column = "map"
|
||||||
|
let startsWith = ""
|
||||||
|
|
||||||
|
if (req.query.limit)
|
||||||
|
limit = req.query.limit
|
||||||
|
|
||||||
|
if (req.query.sortBy)
|
||||||
|
sortBy = req.query.sortBy
|
||||||
|
|
||||||
|
if (req.query.order)
|
||||||
|
order = req.query.order
|
||||||
|
|
||||||
|
if (req.query.byMapper)
|
||||||
|
column = "mapper"
|
||||||
|
|
||||||
|
/* TODO: do this in one query? */
|
||||||
|
const amount = sqlite.prepare(
|
||||||
|
`SELECT COUNT(*) as amount FROM maps WHERE ${column} LIKE "${query}"`).get()
|
||||||
|
|
||||||
|
let pages = Math.floor(amount.amount / limit)
|
||||||
|
|
||||||
|
if (req.query.page)
|
||||||
|
offset = (req.query.page * limit)
|
||||||
|
|
||||||
|
const maps = sqlite.prepare(
|
||||||
|
`SELECT * FROM maps WHERE ${column} LIKE "${query}" ORDER BY ${sortBy} ${order} LIMIT ? OFFSET ?`).all(limit, offset)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: {
|
||||||
|
amount: amount.amount,
|
||||||
|
pages,
|
||||||
|
maps,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default mapApi
|
91
api/players.js
Normal file
91
api/players.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { Router } from 'express'
|
||||||
|
import { sqlite } from '../db/init.js'
|
||||||
|
|
||||||
|
const playerApi = Router()
|
||||||
|
|
||||||
|
|
||||||
|
playerApi.get(
|
||||||
|
'/get/:player',
|
||||||
|
async (req, res) => {
|
||||||
|
let player = req.params.player
|
||||||
|
|
||||||
|
/* Misc */
|
||||||
|
const firstFinish = sqlite.prepare(`SELECT server as server, map as map, time as time, Timestamp as date FROM race WHERE name = ? ORDER BY Timestamp ASC LIMIT 1`).get(player)
|
||||||
|
|
||||||
|
/* TODO, make points a single table. Would be alot more efficent but longer cache creation time */
|
||||||
|
/* Points */
|
||||||
|
const points = sqlite.prepare(`SELECT rank, points FROM points WHERE name = ?`).get(player)
|
||||||
|
const pointsRank = sqlite.prepare(`SELECT rank, points FROM pointsRank WHERE name = ?`).get(player)
|
||||||
|
const pointsTeam = sqlite.prepare(`SELECT rank, points FROM pointsTeam WHERE name = ?`).get(player)
|
||||||
|
|
||||||
|
const pointsThisWeek = sqlite.prepare(`SELECT rank, points FROM pointsThisWeek WHERE name = ?`).get(player)
|
||||||
|
const pointsThisMonth = sqlite.prepare(`SELECT rank, points FROM pointsThisMonth WHERE name = ?`).get(player)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: {
|
||||||
|
firstFinish,
|
||||||
|
|
||||||
|
points,
|
||||||
|
pointsRank,
|
||||||
|
pointsTeam,
|
||||||
|
|
||||||
|
pointsThisWeek,
|
||||||
|
pointsThisMonth,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Searching allows you to attach sql LIKE %
|
||||||
|
* Example to search for players beginning with Test
|
||||||
|
* You can do search for Test%
|
||||||
|
*/
|
||||||
|
playerApi.get(
|
||||||
|
'/search',
|
||||||
|
async (req, res) => {
|
||||||
|
/* Check if a query was provided */
|
||||||
|
if (!req.query.q) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
response: "No query ('host/path?q=query') provided!"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
1
|
||||||
|
let name = req.query.q
|
||||||
|
|
||||||
|
/* Set defaults */
|
||||||
|
let limit = 20
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
if (req.query.limit) {
|
||||||
|
limit = req.query.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = sqlite.prepare(
|
||||||
|
`SELECT COUNT(*) as amount FROM points
|
||||||
|
WHERE name LIKE "${name}"
|
||||||
|
`).get()
|
||||||
|
|
||||||
|
let pages = Math.floor(amount.amount / limit)
|
||||||
|
|
||||||
|
if (req.query.page)
|
||||||
|
offset = (req.query.page * limit)
|
||||||
|
|
||||||
|
const players = sqlite.prepare(
|
||||||
|
`SELECT Rank, Name, Points FROM points
|
||||||
|
WHERE name LIKE "${name}" LIMIT ? OFFSET ?
|
||||||
|
`).all(limit, offset)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
response: {
|
||||||
|
amount: amount.amount,
|
||||||
|
pages: pages,
|
||||||
|
players: players,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default playerApi
|
91
db/generate.js
Normal file
91
db/generate.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { sqlite, skinDB } from "./init.js"
|
||||||
|
import tasks from "./tasks.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This constructs the DB with indexes and rankings...
|
||||||
|
* @module db/generateDB
|
||||||
|
*/
|
||||||
|
export function generateDB() {
|
||||||
|
/* TODO: Clean this up as it is a mess */
|
||||||
|
console.log("Generating race index")
|
||||||
|
|
||||||
|
/* Generate race index TODO: Remove useless ones */
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_race_Map_2" ON "race" ("Map","Name")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_race_Name" ON "race" ("Name","Timestamp")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_race_Server" ON "race" ("Server")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_race_MapTimestamp" ON "race" ("Map","Timestamp")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_race_Timestamp" ON "race" ("Timestamp")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_race_MapNameTime" ON "race" ("Map", "Name", "Time")`)
|
||||||
|
|
||||||
|
/* Create rankings table */
|
||||||
|
console.log("Creating rankings table")
|
||||||
|
sqlite.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "rankings" (
|
||||||
|
"Map" varchar(128) NOT NULL,
|
||||||
|
"Name" varchar(16) NOT NULL,
|
||||||
|
"Time" float NOT NULL DEFAULT 0,
|
||||||
|
"Timestamp" timestamp NOT NULL DEFAULT current_timestamp,
|
||||||
|
"Server" char(4) NOT NULL DEFAULT '',
|
||||||
|
"rank" INTEGER NOT NULL);
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log("Calculating rankings for each map")
|
||||||
|
tasks.processRankings()
|
||||||
|
|
||||||
|
/* Generate rankings index */
|
||||||
|
console.log("Generating rankings index")
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_rankings_map" ON "rankings" ("Map")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_rankings_rank" ON "rankings" ("rank")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_rankings_player" ON "rankings" ("Name")`)
|
||||||
|
|
||||||
|
/* Generate teamrace index */
|
||||||
|
console.log("Generating teamrace index")
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_teamrace_Map" ON "teamrace" ("Map")`);
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_teamrace_ID" ON "teamrace" ("ID")`);
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_teamrace_MapID" ON "teamrace" ("Map", "ID")`);
|
||||||
|
|
||||||
|
console.log("Creating teamrankings table")
|
||||||
|
sqlite.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "teamrankings" (
|
||||||
|
"Map" varchar(128) NOT NULL,
|
||||||
|
"ID" varbinary(16) NOT NULL,
|
||||||
|
"Name" varchar(16) NOT NULL,
|
||||||
|
"Time" float NOT NULL DEFAULT 0,
|
||||||
|
"Timestamp" timestamp NOT NULL DEFAULT current_timestamp,
|
||||||
|
"Server" char(4) NOT NULL DEFAULT '',
|
||||||
|
"teamrank" INTEGER NOT NULL);
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log("Calculating teamrankings for each map")
|
||||||
|
tasks.processTeamRankings()
|
||||||
|
|
||||||
|
console.log("Generating teamrankings index")
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_teamrankings_map" ON "teamrankings" ("Map")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_teamrankings_rank" ON "teamrankings" ("teamrank")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_teamrankings_player" ON "teamrankings" ("name")`)
|
||||||
|
|
||||||
|
sqlite.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "points" (
|
||||||
|
"rank" INTEGER NOT NULL,
|
||||||
|
"name" varchar(16) NOT NULL,
|
||||||
|
"points" INTEGER NOT NULL);
|
||||||
|
`)
|
||||||
|
|
||||||
|
/* Process all types of points */
|
||||||
|
console.log("Inserting points to DB")
|
||||||
|
tasks.processAllPoints()
|
||||||
|
|
||||||
|
skinDB.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "skindata" (
|
||||||
|
"timestamp" INTEGER NOT NULL,
|
||||||
|
"player" varchar(16) NOT NULL,
|
||||||
|
"clan" varchar(12) NOT NULL,
|
||||||
|
"flag" INTEGER NOT NULL,
|
||||||
|
"skin" varchar(16) NOT NULL,
|
||||||
|
"useColor" INTEGER NOT NULL,
|
||||||
|
"colorBodyRaw" INTEGER NOT NULL,
|
||||||
|
"colorBodyHex" varchar(8) NOT NULL,
|
||||||
|
"colorFeetRaw" INTEGER NOT NULL,
|
||||||
|
"colorFeetHex" varchar(8) NOT NULL);
|
||||||
|
`)
|
||||||
|
}
|
26
db/init.js
Normal file
26
db/init.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import Database from 'better-sqlite3'
|
||||||
|
|
||||||
|
/* Export DB for use in other files */
|
||||||
|
export let sqlite = undefined
|
||||||
|
export let skinDB = undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This initalizes the ddnet.sqlite and skindata.sqlite DB...
|
||||||
|
* @module db/dbInit
|
||||||
|
*/
|
||||||
|
export function dbInit() {
|
||||||
|
console.log("Starting up databases...")
|
||||||
|
|
||||||
|
/* load in db using better-sqlite3 */
|
||||||
|
sqlite = new Database('ddnet.sqlite', { verbose: console.log });
|
||||||
|
skinDB = new Database('skindata.sqlite', { });
|
||||||
|
|
||||||
|
/* WAL mode */
|
||||||
|
sqlite.pragma('journal_mode = WAL');
|
||||||
|
|
||||||
|
/* Unsafe mode */
|
||||||
|
sqlite.unsafeMode()
|
||||||
|
|
||||||
|
console.log("Loaded in 'ddnet.sqlite'!")
|
||||||
|
console.log("Loaded in 'skindata.sqlite'!")
|
||||||
|
}
|
143
db/tasks.js
Normal file
143
db/tasks.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import msgpack from '@msgpack/msgpack'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
import { sqlite } from "./init.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module parses the msgpack provided by DDNet...
|
||||||
|
* @module db/decodeMsgpack
|
||||||
|
*/
|
||||||
|
export function decodeMsgpack() {
|
||||||
|
const data = fs.readFileSync('players.msgpack')
|
||||||
|
const decoded = msgpack.decodeMulti(data, {wrap: true})
|
||||||
|
const order = ['categories', 'maps', 'totalPoints', 'pointsRanks', 'pointsThisWeek', 'pointsThisMonth', 'teamRankPoints', 'rankPoints', 'serverRanks']
|
||||||
|
let final = {}
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
for (const part of decoded) {
|
||||||
|
final[order[i]] = part
|
||||||
|
++i
|
||||||
|
}
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This generates rankings for each map...
|
||||||
|
* @module db/processRankings
|
||||||
|
*/
|
||||||
|
export function processRankings() {
|
||||||
|
const maps = sqlite.prepare(`SELECT map FROM maps`);
|
||||||
|
|
||||||
|
for (const map of maps.iterate()) {
|
||||||
|
sqlite.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO rankings
|
||||||
|
(
|
||||||
|
map, name, time, timestamp, rank, server
|
||||||
|
)
|
||||||
|
SELECT map, name, time, timestamp, rank, server
|
||||||
|
FROM (
|
||||||
|
SELECT rank() OVER w AS rank,
|
||||||
|
map,
|
||||||
|
timestamp,
|
||||||
|
NAME,
|
||||||
|
min(time) AS time,
|
||||||
|
server
|
||||||
|
FROM race
|
||||||
|
WHERE map = ?
|
||||||
|
GROUP BY NAME window w AS (ORDER BY time) ) AS a
|
||||||
|
ORDER BY rank
|
||||||
|
`).run(map.Map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This generates teamrankings for each map...
|
||||||
|
* @module db/processTeamRankings
|
||||||
|
*/
|
||||||
|
export function processTeamRankings() {
|
||||||
|
const maps = sqlite.prepare(`SELECT map FROM maps`);
|
||||||
|
|
||||||
|
for (const map of maps.iterate()) {
|
||||||
|
sqlite.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO teamrankings
|
||||||
|
(
|
||||||
|
name, map, id, time, timestamp, server, teamrank
|
||||||
|
)
|
||||||
|
SELECT DISTINCT(r.NAME),
|
||||||
|
r.map, r.id, r.time, r.timestamp,
|
||||||
|
Substring(n.server, 1, 3),
|
||||||
|
dense_rank() OVER w AS rank
|
||||||
|
FROM ((
|
||||||
|
SELECT DISTINCT id
|
||||||
|
FROM teamrace
|
||||||
|
WHERE map = ?
|
||||||
|
ORDER BY time) AS l
|
||||||
|
)
|
||||||
|
LEFT JOIN teamrace AS r
|
||||||
|
ON l.id = r.id
|
||||||
|
INNER JOIN race AS n
|
||||||
|
ON r.map = n.map
|
||||||
|
AND r.NAME = n.NAME
|
||||||
|
AND r.time = n.time window w AS (ORDER BY r.time)
|
||||||
|
`).run(map.Map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This inserts all types of points into a table...
|
||||||
|
* @module db/processAllPoints
|
||||||
|
*/
|
||||||
|
export function processAllPoints() {
|
||||||
|
const msgpack = decodeMsgpack()
|
||||||
|
|
||||||
|
let types = {
|
||||||
|
points: msgpack.pointsRanks,
|
||||||
|
pointsThisWeek: msgpack.pointsThisWeek,
|
||||||
|
pointsThisMonth: msgpack.pointsThisMonth,
|
||||||
|
pointsTeam: msgpack.teamRankPoints,
|
||||||
|
pointsRank: msgpack.rankPoints,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate tables */
|
||||||
|
for(const type in types) {
|
||||||
|
sqlite.exec(
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS "${type}"
|
||||||
|
(
|
||||||
|
"rank" INTEGER NOT NULL,
|
||||||
|
"name" varchar(16) NOT NULL,
|
||||||
|
"points" INTEGER NOT NULL);
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Insert data */
|
||||||
|
for(const type in types) {
|
||||||
|
let rank = 1
|
||||||
|
|
||||||
|
for (const entry of types[type]) {
|
||||||
|
sqlite.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO "${type}"
|
||||||
|
(
|
||||||
|
rank, name, points
|
||||||
|
) VALUES (?, ?, ?)`).run(
|
||||||
|
rank, entry[0], entry[1])
|
||||||
|
|
||||||
|
++rank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate indexes */
|
||||||
|
for(const type in types) {
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "idx_${type}_player" ON "${type}" ("Name")`)
|
||||||
|
sqlite.exec(`CREATE INDEX IF NOT EXISTS "Idx_${type}_rank" on "${type}" ("Rank")`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
processAllPoints,
|
||||||
|
processRankings,
|
||||||
|
processTeamRankings
|
||||||
|
}
|
3
ddnet-links.txt
Normal file
3
ddnet-links.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
http://ddnet.tw/players.msgpack
|
||||||
|
https://ddnet.tw/status/index.json
|
||||||
|
https://ddnet.tw/stats/ddnet.sqlite.zip
|
102
ddnss/handler.js
Normal file
102
ddnss/handler.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { skinDB } from "../db/init.js"
|
||||||
|
|
||||||
|
export async function ddnssStart() {
|
||||||
|
const response = await fetch('https://ddnet.tw/status/index.json');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
for (const servers of data) {
|
||||||
|
/* Check if server isn't empty and not full */
|
||||||
|
if (servers.num_clients > 0 && servers.num_clients < 59) {
|
||||||
|
let server = `${servers.ip}:${servers.port}`
|
||||||
|
console.log(`Connecting: ${servers.ip}:${servers.port}`)
|
||||||
|
|
||||||
|
/* exec ddnss */
|
||||||
|
await scrapeServer(`${server}`)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
console.log(`Server full: ${servers.ip}:${servers.port}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scrapeServer(server) {
|
||||||
|
let command = `./ddnss/build/DDNet "ui_server_address ${server}" -f ddnss/build/config.conf`
|
||||||
|
let skinData
|
||||||
|
|
||||||
|
return new Promise((done, failed) => {
|
||||||
|
exec(command, { encoding: 'utf8', timeout: 10000 }, (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
err.stdout = stdout
|
||||||
|
err.stderr = stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle error from parsing of JSON */
|
||||||
|
try {
|
||||||
|
skinData = JSON.parse(stdout)
|
||||||
|
} catch (e) {
|
||||||
|
done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skinData === null) {
|
||||||
|
done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get timestamp */
|
||||||
|
let ts = (Date.now())
|
||||||
|
|
||||||
|
/* Insert skindata */
|
||||||
|
for (const entry of skinData) {
|
||||||
|
skinDB.prepare(`INSERT INTO "skindata"
|
||||||
|
(
|
||||||
|
timestamp,
|
||||||
|
player,
|
||||||
|
clan,
|
||||||
|
flag,
|
||||||
|
skin,
|
||||||
|
useColor,
|
||||||
|
colorBodyRaw,
|
||||||
|
colorBodyHex,
|
||||||
|
colorFeetRaw,
|
||||||
|
ColorFeetHex
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).
|
||||||
|
run(
|
||||||
|
ts,
|
||||||
|
entry.player,
|
||||||
|
entry.clan,
|
||||||
|
entry.flag,
|
||||||
|
entry.skindata.skin,
|
||||||
|
entry.skindata.useColor,
|
||||||
|
entry.skindata.colorBody.raw,
|
||||||
|
entry.skindata.colorBody.hex,
|
||||||
|
entry.skindata.colorFeet.raw,
|
||||||
|
entry.skindata.colorFeet.hex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
|
||||||
|
/* A bit hacky way of killing ddnss */
|
||||||
|
//exec(`pkill -9 -f ddnss`)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CREATE TABLE IF NOT EXISTS "skindata"
|
||||||
|
(
|
||||||
|
"timestamp" INTEGER NOT NULL,
|
||||||
|
"player" varchar(16) NOT NULL,
|
||||||
|
"clan" varchar(12) NOT NULL,
|
||||||
|
"flag" INTEGER NOT NULL,
|
||||||
|
"skin" varchar(16) NOT NULL,
|
||||||
|
"useColor" INTEGER NOT NULL,
|
||||||
|
"colorBodyRaw" INTEGER NOT NULL,
|
||||||
|
"colorBodyHex" varchar(8) NOT NULL,
|
||||||
|
"colorFeetRaw" INTEGER NOT NULL,
|
||||||
|
"colorFeetHex" varchar(8) NOT NULL);
|
||||||
|
*/
|
1
dotenv-template
Normal file
1
dotenv-template
Normal file
|
@ -0,0 +1 @@
|
||||||
|
PORT = 12345
|
28
index.js
Normal file
28
index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import express from 'express'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import api from './api/api.js'
|
||||||
|
import { generateDB } from "./db/generate.js"
|
||||||
|
import { sqlite, dbInit } from "./db/init.js"
|
||||||
|
import { ddnssStart, scrapeServer } from './ddnss/handler.js'
|
||||||
|
|
||||||
|
/* Read the .env file */
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
/* Init db */
|
||||||
|
dbInit()
|
||||||
|
|
||||||
|
/* check if the table points exists */
|
||||||
|
const exists = sqlite.prepare(`SELECT count(*) as a FROM sqlite_master WHERE type='table' AND name='points'`).get()
|
||||||
|
|
||||||
|
/* Generate the DB if false */
|
||||||
|
if(!exists.a)
|
||||||
|
generateDB()
|
||||||
|
|
||||||
|
|
||||||
|
/* Init express */
|
||||||
|
const Server = express()
|
||||||
|
Server.use('/api', api)
|
||||||
|
|
||||||
|
Server.listen(process.env.PORT, () => console.log(`Server started and listening on port ${process.env.PORT}.`))
|
||||||
|
|
||||||
|
//ddnssStart()
|
20
package.json
Normal file
20
package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "ddstats-server-sqlite",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@msgpack/msgpack": "^2.7.1",
|
||||||
|
"better-sqlite3": "^7.4.3",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"mime-types": "^2.1.33",
|
||||||
|
"node-fetch": "^3.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"type": "module",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user