Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

14 changed files with 369 additions and 677 deletions

3
.gitignore vendored
View file

@ -3,5 +3,4 @@ node_modules
built built
config/config.json config/config.json
*.log *.log
.env .env
storage/files/*

View file

@ -1,8 +1,6 @@
![in the database 2](./assets/full.png "in the database 2") ![in the database 2](./assets/full.png "in the database 2")
a database site for notitg modcharts, currently very very unfinished a database site for notitg modcharts, currently very very unfinished, basically just a boilerplate
you can login with discord, upload and download files, thats about it, but im still proud of it
## setup ## setup

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

682
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,24 +12,20 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/express": "github:types/express", "@types/express": "github:types/express",
"adm-zip": "^0.5.1", "@types/mongoose": "^5.7.36",
"axios": "^0.20.0", "axios": "^0.20.0",
"connect-mongo": "^3.2.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.2.0", "express-fileupload": "^1.2.0",
"express-session": "^1.17.1", "mongoose": "^5.10.2",
"mongoose": "^5.11.8",
"mongoose-int32": "^0.4.1", "mongoose-int32": "^0.4.1",
"serve-favicon": "^2.5.0", "node-stream-zip": "^1.11.3",
"typescript": "^4.1.3", "typescript": "^4.0.2",
"uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/express-session": "^1.17.3", "@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/eslint-plugin": "^4.11.0", "@typescript-eslint/parser": "^4.0.1",
"@typescript-eslint/parser": "^4.11.0", "eslint": "^7.8.1"
"eslint": "^7.16.0"
} }
} }

View file

@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Index</title> <title>Index</title>
</head> </head>
<body> <body>
<h1>Hi</h1> <h1>Hi</h1>
</body> </body>
</html> </html>

View file

@ -1,42 +1,36 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>list</title> <title>list</title>
</head> </head>
<body> <body>
<div id="doc-list"> <div id="doc-list">
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/axios@0.20.0/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@0.20.0/dist/axios.min.js"></script>
<script> <script>
axios.get('/api/list').then(({ data }) => { axios.get('/api/list').then(({ data }) => {
console.log(data); console.log(data);
const el = document.getElementById('doc-list'); const el = document.getElementById('doc-list');
for (const doc of data) { for (const doc of data) {
let p = document.createElement('p'); let p = document.createElement('p');
p.innerHTML = `<b>${doc.artist} - ${doc.title}</b> by ${doc.credit}\nuploaded by ${doc.uploaderJSON.username}#${doc.uploaderJSON.discriminator}\n<a href="files/${doc.id}.zip">download</a>`; p.innerText = `${doc.artist} - ${doc.title} by ${doc.credit}`;
el.insertAdjacentElement('beforeend', p);
if (doc.editable) { let charts = document.createElement('ul');
p.innerHTML += ` <a href="../${doc.id}/edit">edit</a>` for (const chart of doc.charts) {
} let l = document.createElement('li');
l.innerText = `${chart.difficulty} ${chart.rating} - ${chart.name}`
el.insertAdjacentElement('beforeend', p); charts.insertAdjacentElement('beforeend', l);
}
let charts = document.createElement('ul'); el.insertAdjacentElement('beforeend', charts);
for (const chart of doc.charts) { }
let l = document.createElement('li'); });
l.innerHTML = `${chart.difficulty} ${chart.rating} - <b>${chart.name}</b><br>` + </script>
`${chart.steps} steps, ${chart.mines} mines, ${chart.jumps} jumps, ${chart.hands} hands, ${chart.holds} holds, ${chart.rolls} rolls` </body>
charts.insertAdjacentElement('beforeend', l);
}
el.insertAdjacentElement('beforeend', charts);
}
});
</script>
</body>
</html> </html>

View file

@ -19,7 +19,7 @@
const file = document.getElementById('file'); const file = document.getElementById('file');
if (file.files.length) { if (file.files.length) {
console.log(file.files[0]); console.log(file.files[0]);
const formData = new FormData(); const formData = new FormData();
formData.append('file', file.files[0]); formData.append('file', file.files[0]);
try { try {

View file

@ -1,6 +1,3 @@
import { User } from './schema';
import * as uuid from 'uuid';
const API_ENDPOINT = 'https://discord.com/api/v6'; const API_ENDPOINT = 'https://discord.com/api/v6';
const axios = require('axios').default; const axios = require('axios').default;
@ -8,7 +5,6 @@ const axios = require('axios').default;
export function run(app) { export function run(app) {
app.get('/discordauth', async (req, res) => { app.get('/discordauth', async (req, res) => {
const code = req.query.code; const code = req.query.code;
const url = `http://${req.headers.host}/discordauth`;
if (code) { if (code) {
try { try {
@ -17,7 +13,7 @@ export function run(app) {
client_secret: process.env.DISCORD_OAUTH_CLIENTSECRET, client_secret: process.env.DISCORD_OAUTH_CLIENTSECRET,
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: code, code: code,
redirect_uri: url, redirect_uri: 'http://localhost:8080/discordauth',
scope: 'identify' scope: 'identify'
}); });
@ -32,49 +28,12 @@ export function run(app) {
authorization: `${postRes.data.token_type} ${postRes.data.access_token}` authorization: `${postRes.data.token_type} ${postRes.data.access_token}`
} }
}); });
res.send(`hi ${userInfo.data.username}#${userInfo.data.discriminator}<br><img src="https://media.discordapp.net/avatars/${userInfo.data.id}/${userInfo.data.avatar}.png">`);
const users = await User.find({id: String(userInfo.data.id)});
let userUuid = '';
if (users.length === 0) {
let newUuid = uuid.v4();
while (User.find({uuid: newUuid})[0]) {
newUuid = uuid.v4();
}
const newUser = new User({
id: String(userInfo.data.id),
createdAt: new Date(),
username: userInfo.data.username,
discriminator: userInfo.data.discriminator,
avatar: userInfo.data.avatar,
uuid: newUuid,
});
userUuid = newUser.get('uuid');
newUser.save();
} else {
const user = users[0];
userUuid = user.get('uuid');
user.set('id', String(userInfo.data.id));
user.set('username', userInfo.data.username);
user.set('discriminator', userInfo.data.discriminator);
user.set('avatar', userInfo.data.avatar);
}
req.session!.discord = userInfo.data;
req.session!.uuid = userUuid;
res.send(`logged in as ${userInfo.data.username}#${userInfo.data.discriminator}<br><img src="https://media.discordapp.net/avatars/${userInfo.data.id}/${userInfo.data.avatar}.png"><br>ur useruuid is ${userUuid}`);
} catch(err) { } catch(err) {
res.send(`whoooops<br>${err}`); res.send(`whoooops<br>${err}`);
console.error(err);
} }
} else { } else {
res.send(`<a href="https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_OAUTH_CLIENTID}&redirect_uri=${encodeURI(url)}&response_type=code&scope=identify">Click here!!</a>`); res.send(`<a href="https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_OAUTH_CLIENTID}&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fdiscordauth&response_type=code&scope=identify">Click here!!</a>`);
} }
}); });
} }

View file

@ -3,12 +3,9 @@ import * as mongoose from 'mongoose';
import * as fs from 'fs'; import * as fs from 'fs';
import * as winston from 'winston'; import * as winston from 'winston';
import * as fileUpload from 'express-fileupload'; import * as fileUpload from 'express-fileupload';
import * as session from 'express-session';
import * as favicon from 'serve-favicon';
const MongoStore = require('connect-mongo')(session);
import * as format from './lib/format'; import * as format from './lib/format';
import { File, User } from './schema'; import { File } from './schema';
import * as upload from './upload'; import * as upload from './upload';
import * as auth from './auth'; import * as auth from './auth';
@ -55,22 +52,8 @@ db.then(() => {
// @ts-ignore // @ts-ignore
app.use(express.urlencoded({extended: true})); app.use(express.urlencoded({extended: true}));
app.use(favicon('assets/icon.ico'));
app.use(fileUpload({limits: { fileSize: 50 * 1024 * 1024 }})); app.use(fileUpload({limits: { fileSize: 50 * 1024 * 1024 }}));
app.use(express.static('public', {extensions: ['html', 'htm']})); app.use(express.static('public', {extensions: ['html', 'htm']}));
app.use(express.static('storage', {extensions: ['zip']}));
app.use(session({
name: 'funnyuserdata',
secret: 'wenis',
store: new MongoStore({ mongooseConnection: mongoose.connection }),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
httpOnly: true,
sameSite: 'lax',
},
resave: false,
saveUninitialized: true
}));
app.use('/assets', express.static('assets')); app.use('/assets', express.static('assets'));
app.set('db', db); app.set('db', db);
@ -80,23 +63,8 @@ db.then(() => {
upload.run(app); upload.run(app);
auth.run(app); auth.run(app);
app.get('/api/list', async (req, res) => { app.get('/api/list', async (req, res) => { // only for testing
const files = await File.find({}); const docs = await File.find({});
const docs = [];
for (const doc of files) {
const d: any = doc.toJSON();
d.editable = false;
if (req.session) d.editable = req.session.uuid === d.uploader;
const user = await User.find({uuid: d.uploader});
if (user) {
d.uploaderJSON = user[0].toJSON(); // this is built upon 20 layers of metajank and i despise it
docs.push(d);
}
}
// TODO: filter out _id and __v? possibly more // TODO: filter out _id and __v? possibly more
res.send(docs); res.send(docs);
}); });

View file

@ -1,4 +1,6 @@
export function parseSM(data: string) { export function parseSM(data: string) {
data = data.replace(/[\n\r]/g,'');
// steps // steps
const difficulties = []; const difficulties = [];
const steps = data.split('#NOTES:'); const steps = data.split('#NOTES:');
@ -16,25 +18,11 @@ export function parseSM(data: string) {
diff.rating = Number(stepsSplit[3]); diff.rating = Number(stepsSplit[3]);
diff.radarvalues = stepsSplit[4].split(',').map(v => Number(v)); diff.radarvalues = stepsSplit[4].split(',').map(v => Number(v));
const chart = stepsSplit[5];
diff.rawChart = chart;
diff.steps = chart.split(/[124]/g).length - 1;
diff.mines = chart.split('M').length - 1;
diff.jumps = chart.split(/[124]0{0,2}[124]/g).length - 1;
diff.hands = chart.split(/[124]0{0,1}[124]0{0,1}[124]/g).length - 1;
diff.holds = chart.split('2').length - 1;
diff.rolls = chart.split('4').length - 1;
diff.steps -= diff.jumps; // jumps are counted as 1 step
difficulties.push(diff); difficulties.push(diff);
} }
} }
} }
data = data.replace(/[\n\r]/g,'');
// metadata // metadata
const lines = data.split(';').filter(l => l.startsWith('#')); const lines = data.split(';').filter(l => l.startsWith('#'));
const obj: any = {}; const obj: any = {};
@ -53,7 +41,7 @@ export function parseSM(data: string) {
const map = {}; const map = {};
for (const i in keys) { for (const i in keys) {
map[String(Number(keys[i])).replace('.', ',')] = Number(values[i]); // afaik maps are only numbers? map[Number(keys[i])] = Number(values[i]); // afaik maps are only numbers?
} }
value = map; value = map;

10
src/lib/util.ts Normal file
View file

@ -0,0 +1,10 @@
import * as fs from 'fs';
export function returnStatic(page) {
return (req, res) => {
fs.readFile(`src/html/${page}`, 'utf8', (err, data) => {
if (err) throw err;
res.send(data);
});
};
}

View file

@ -1,118 +1,39 @@
/* eslint-disable no-unused-vars */
import * as mongoose from 'mongoose'; import * as mongoose from 'mongoose';
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
export enum SMVersion {
OPENITG,
FUCKEXE,
NOTITG_V1,
NOTITG_V2,
NOTITG_V3,
NOTITG_V3_1,
NOTITG_V4,
NOTITG_V4_0_1,
STEPMANIA_3_95,
STEPMANIA_5_0,
STEPMANIA_5_1,
STEPMANIA_5_2,
STEPMANIA_5_3,
}
const Sample = new Schema({ const Sample = new Schema({
start: {type: Number, default: 0}, start: {type: Number, default: 0},
length: {type: Number, default: 0} length: {type: Number, default: 0}
}); });
const UserRating = new Schema({
rating: {type: Number, default: 0},
createdAt: Date,
user: {type: String, default: '00000000-0000-4000-a000-000000000000'}
});
const Chart = new Schema({ const Chart = new Schema({
type: {type: String, default: 'dance-single'}, type: {type: String, default: 'dance-single'},
name: {type: String, default: ''}, name: {type: String, default: ''},
difficulty: {type: String, default: 'Challenge'}, difficulty: {type: String, default: 'Challenge'},
radarvalues: [Number],
rating: {type: Number, default: 0}, rating: {type: Number, default: 0},
ratingsVote: {type: [UserRating], default: []}, radarvalues: [Number]
spoilered: {type: Boolean, default: false},
hidden: {type: Boolean, default: false},
steps: {type: Number, default: 0},
mines: {type: Number, default: 0},
jumps: {type: Number, default: 0},
hands: {type: Number, default: 0},
holds: {type: Number, default: 0},
rolls: {type: Number, default: 0},
});
const Comment = new Schema({
author: {type: String, default: '00000000-0000-4000-a000-000000000000'},
createdAt: Date,
content: {type: String, default: ''}
}); });
const FileSchema = new Schema({ const FileSchema = new Schema({
id: {type: Number, default: 0},
title: {type: String, default: 'unknown'}, title: {type: String, default: 'unknown'},
titleTranslit: String, titleTranslit: String,
artist: {type: String, default: 'unknown'}, artist: {type: String, default: 'unknown'},
artistTranslit: String, artistTranslit: String,
subtitle: String, subtitle: String,
subtitleTranslit: String, subtitleTranslit: String,
credit: String, credit: String,
uploader: {type: String, default: '00000000-0000-4000-a000-000000000000'}, uploader: {type: String, default: '00000000-0000-4000-a000-000000000000'},
sample: Sample, sample: Sample,
bpms: {type: Object, default: {'0': 0}}, bpms: {type: Object, default: {'0': 0}},
charts: [Chart]
charts: {type: [Chart], default: []},
description: {type: String, default: ''},
createdAt: Date,
smVersion: {type: Number, default: 0}, // see SMVersion enum
ytLink: String,
customLink: String,
hidden: {type: Boolean, default: false},
comments: {type: [Comment], default: []},
}); });
export const File = mongoose.model('File', FileSchema); export const File = mongoose.model('File', FileSchema);
const UserSchema = new Schema({ // this is pretty much just a discord user lol const UserSchema = new Schema({ // this is pretty much just a discord user lol
id: {type: String, default: 'notgiven!!!!!!!!!!!!'}, // discord id, cus longass number id: String, // cus longass number
createdAt: Date, approved: Boolean
// caching
username: {type: String, default: 'User'},
discriminator: {type: String, default: '0000'},
avatar: String,
// used internally
uuid: {type: String, default: '00000000-0000-4000-a000-000000000000'},
approvedUpload: {type: Boolean, default: false},
approvedRate: {type: Boolean, default: false},
approvedComment: {type: Boolean, default: false},
}); });
export const User = mongoose.model('User', UserSchema); export const User = mongoose.model('User', UserSchema);
const PackSchema = new Schema({
author: {type: String, default: '00000000-0000-4000-a000-000000000000'},
files: {type: [Number], default: []}, // ids
name: {type: String, default: 'Pack'},
description: {type: String, default: ''},
createdAt: Date,
hidden: {type: Boolean, default: false},
});
export const Pack = mongoose.model('Pack', PackSchema);

View file

@ -1,75 +1,54 @@
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import * as fs from 'fs'; import * as fs from 'fs';
import * as AdmZip from 'adm-zip'; const StreamZip = require('node-stream-zip');
import { returnStatic } from './lib/util';
import { parseSM } from './lib/smparse'; import { parseSM } from './lib/smparse';
import { File, User } from './schema'; import { File } from './schema';
export function run(app) { export function run(app) {
const logger = app.get('logger'); const logger = app.get('logger');
app.post('/api/upload', async (req, res) => { // only for testing, very abusable app.post('/api/upload', async (req, res) => { // only for testing, very abusable
if (!req.files) return res.status(400).send('No files were given'); if (!req.files) return res.status(400).send('No files were given');
if (!req.session.uuid) return res.status(401).send('Not authorized, use /discordauth');
const user = (await User.find({uuid: req.session.uuid}))[0];
if (!user) return res.status(401).send('User doesn\'t exist, try re-logging in');
if (!user.get('approvedUpload')) return res.status(403).send('Your account is not allowed to upload files! Contact a moderator to verify your account');
const file = req.files.file; const file = req.files.file;
if (file.mimetype !== 'application/zip' && file.mimetype !== 'application/x-zip-compressed') return res.status(400).send('Invalid filetype'); if (file.mimetype !== 'application/zip' && file.mimetype !== 'application/x-zip-compressed') return res.status(400).send('Invalid filetype');
const dir = tmpdir() + '/' + file.md5; const dir = tmpdir() + '/' + file.md5;
fs.writeFile(dir, file.data, async (err) => { fs.writeFile(dir, file.data, (err) => {
if (err) throw err; if (err) throw err;
try { const zip = new StreamZip({
const zip = new AdmZip(dir); file: dir,
const zipEntries = zip.getEntries(); storeEntries: true
});
const smFile: any = Object.values(zipEntries).find((f: any) => zip.on('ready', () => {
!f.isDirectory && (f.entryName.endsWith('.sm')) const smFile = Object.values(zip.entries()).find((f: any) =>
!f.isDirectory && (f.name.endsWith('.sm'))
); );
if (!smFile) { if (!smFile) {
res.status(400).send('No .sm found'); res.status(400).send('No .sm found');
} else { } else {
const data = smFile.getData().toString('utf8'); const data = zip.entryDataSync((smFile as any).name);
const chart = parseSM(data.toString()); const chart = parseSM(data.toString());
logger.info(`${chart.artist} - ${chart.title} was just uploaded`); logger.info(`${chart.artist} - ${chart.title} was just uploaded`);
let id = 0; const file = new File(chart);
for (const f of await File.find({})) { file.save();
id = Math.max(Number(f.id), id);
}
chart.id = id + 1;
chart.uploader = req.session.uuid; // TODO: filter out _id and __v? possibly more
res.send(chart);
chart.createdAt = new Date();
fs.writeFile('./storage/files/' + (id + 1) + '.zip', file.data, (err) => {
if (err) throw err;
const file = new File(chart);
file.save();
// TODO: filter out _id and __v? possibly more
res.send(chart);
});
} }
zip.close();
fs.unlink(dir, (err) => { fs.unlink(dir, (err) => {
if (err) throw err; if (err) throw err;
}); });
} catch(err) { });
logger.error(err.toString());
console.error(err);
res.status(400);
res.send(err.toString());
}
}); });
}); });
} }