Initial commit

This commit is contained in:
2023-11-03 21:12:55 +01:00
parent a4afe6cc85
commit 57362e7520
39 changed files with 3505 additions and 1 deletions
+29 -1
View File
@@ -1,2 +1,30 @@
# url-shortener-public
# URL Shortener
This is a small project i worked on out of boredom.
Its not meant to be perfect, just an idea i had and wanted to work on to kill some time.
Its an url shortener that is meant to allow people to easily create shorted urls, even automate this task if need be.
## Setup
Run `git clone https://git.dommymommy.xyz/emily/url-shortener.git`
Next run `cd url-shortener` and then run `npm i`
Run the sql file on your database server and add the credentials to `./config/main.json`
Now you can run `node main` or `npm start` to start up the service
## Usage
## API
### POST
>/api/v1/createKey
Get Info about a shortcode
>
### GET
>/api/v1/info/\<code>
Get Info about a shortcode
### DELETE
+28
View File
@@ -0,0 +1,28 @@
{
"baseUrl":[
"https://shorten.dommymommy.xyz",
"https://shorten.gofuckaduck.eu",
"https://shorten.kitsunes.eu"
],
"database":{
"host": "the ip of your database",
"port": "the port",
"username": "the username",
"password": "the password",
"database": "url_shortener"
},
"ratelimit":{
"enabled":true,
"hitsUntilBan":5,
"resetHits":true,
"resetTimeframe":5,
"devKey":"a key used in the header to bypass ratelimiting"
},
"banhandler":{
"whitelisted":[
"ips you want to whitelist and prevent from being banned",
]
}
}
+40
View File
@@ -0,0 +1,40 @@
module.exports = {
name:"authenticate",
async function(req, res){
let username = req.body.username.trim()
let password = req.body.password
if(username == undefined || password == undefined || username == ""){
res.json({code:32})
return
}
if(username.length > 32 || username.length < 3){
res.json({code:32})
return
}
let user = await commands.get("query").function("SELECT dtPassword from tbl_users WHERE dtUsername = ?",[username])
if(user.length == 0){
res.json({code:32})
return
}
let valid = await bcrypt.compare(password, user[0].dtPassword)
if(!valid){
res.json({code:31})
return
}
// Yes, i am using bcrypt to make the hash key cause i m too lazy to implement md5 here
// I mean, afterall, it doesnt contain sensitive information, so, who cares what algorithm you use
// it looks fancy and does its job, so, cry about it
let apiKey = await bcrypt.hash(Math.random() * 100000000 * Math.random() + username,saltRounds)
commands.get("query").function("UPDATE tbl_users SET dtApiKey = ? WHERE dtUsername = ?",[apiKey, username]).then(resp => {
res.json({code:30,key:apiKey})
}).catch(err => {
console.log(err);
res.json({code:33})
})
}
}
+37
View File
@@ -0,0 +1,37 @@
module.exports = {
name:"codeList",
async function(req, res){
let apiKey = req.headers.auth
if(apiKey == undefined){
res.json({code:51})
return
}
var userid
await commands.get("validate").check(apiKey).then(resp => {
userid = resp
}).catch(err => {
switch(err){
case 1: res.json({code:51}); break;
case 2: res.json({code:52}); break;
}
return
})
let output = {
code:50,
shortcodes:[]
}
let codes = await commands.get("query").function("SELECT idShortcode, dtOriginal, tbl_urls.dtTimestamp from tbl_urls, tbl_users where fiUser = idUser AND dtApiKey = ?",[apiKey])
for (let i = 0; i < codes.length; i++) {
output.shortcodes.push({
"shortcode": codes[i].idShortcode,
"destination": codes[i].dtOriginal,
"timestamp": codes[i].dtTimestamp
})
}
res.json(output)
}
}
+49
View File
@@ -0,0 +1,49 @@
module.exports = {
name:"createCode",
async function(req, res){
let apiKey = req.headers.auth
let url = req.body.url
if(apiKey == undefined){
res.json({code:41})
return
}
var userid
await commands.get("validate").check(apiKey).then(resp => {
userid = resp
}).catch(err => {
switch(err){
case 1: res.json({code:41}); break;
case 2: res.json({code:42}); break;
}
return
})
let dupes = await commands.get("query").function("SELECT idShortcode FROM tbl_urls, tbl_users WHERE fiUser = idUser AND dtApiKey = ? AND dtOriginal = ?",[apiKey,url])
if(dupes.length != 0){
let urls = []
config.baseUrl.forEach(el => {
urls.push(`${el}/${dupes[0].idShortcode}`)
});
res.json({code:44,shortcode:dupes[0].idShortcode,full_url:urls})
return
}
let hashData = new Date().getTime()+""
hashData = parseInt(hashData.slice(6)) + Math.floor((Math.random()*1000)-500)
let hash = hashids.encode(hashData)
// console.log(a);
commands.get("query").function("INSERT INTO tbl_urls (idShortcode, dtOriginal, dtTimestamp, fiUser) VALUES (?,?,?,?)",[hash, url, new Date().getTime(), userid]).then(resp => {
let urls = []
config.baseUrl.forEach(el => {
urls.push(`${el}/${hash}`)
});
res.json({code:40,shortcode:hash,full_url:urls})
}).catch(err => {
res.json({code:43})
})
}
}
+34
View File
@@ -0,0 +1,34 @@
module.exports = {
name:"createUser",
async function(req, res){
let username = req.body.username.trim()
let password = req.body.password
if(username == undefined || passowrd == undefined || username == ""){
res.json({code:24})
return
}
if(username.length > 32 || username.length < 3){
res.json({code:23})
return
}
let user = await commands.get("query").function("SELECT idUser from tbl_users WHERE dtUsername = ?",[username])
if(user.length != 0){
res.json({code:21})
return
}
let hash = await bcrypt.hash(password, saltRounds)
commands.get("query").function("INSERT INTO tbl_users (dtUsername,dtPassword,dtTimestamp) VALUES (?,?,?)",[username,hash,new Date().getTime()],true).then(res => {
console.log(res);
if(res != false){
res.json({code:20})
}
})
.catch(err => {
res.json({code:22,error:err})
})
}
}
+35
View File
@@ -0,0 +1,35 @@
module.exports = {
name:"deleteCode",
async function(req, res){
let apiKey = req.headers.auth
let shortcode = req.body.shortcode
if(apiKey == undefined){
res.json({code:61})
return
}
var userid
await commands.get("validate").check(apiKey).then(resp => {
userid = resp
}).catch(err => {
switch(err){
case 1: res.json({code:61}); break;
case 2: res.json({code:62}); break;
}
return
})
let code = await commands.get("query").function("SELECT idShortcode FROM tbl_urls, tbl_users WHERE fiUser = idUser AND dtApiKey = ? AND idShortcode = ?",[apiKey,shortcode])
if(code.length == 0){
res.json({code:65})
return
}
commands.get("query").function("DELETE FROM tbl_urls WHERE idShortcode = ?",[shortcode]).then(resp => {
res.json({code:60})
}).catch(err => {
res.json({code:63})
})
}
}
+25
View File
@@ -0,0 +1,25 @@
module.exports = {
name:"info",
async function(req, res){
let query = await commands.get("query").function("SELECT * from tbl_urls, tbl_users WHERE idShortcode = ? AND fiUser = idUser",[req.params.shortcode])
if(query.length == 0){
res.json({code:11})
return
}
let query2 = await commands.get("query").function("SELECT COUNT(*) AS amount FROM tbl_access WHERE fiShortcode = ?",[req.params.shortcode])
let query3 = await commands.get("query").function("SELECT COUNT(*) AS amount FROM (SELECT * FROM tbl_access WHERE fiShortcode = ? GROUP BY dtIpHash) AS sq",[req.params.shortcode])
res.json({
code:10,
shortcode: req.params.shortcode,
redirect: query[0].dtOriginal,
creator: query[0].dtUsername,
creationTime: query[0].dtTimestamp,
clicked:{
total:query2[0].amount,
distinct_users:query3[0].amount
}
})
}
}
+22
View File
@@ -0,0 +1,22 @@
const crypto = require('crypto');
module.exports = {
name:"redirect",
async function(req, res){
if(req.params.shortcode == "")
return
if(shortcodes[req.params.shortcode]){
res.redirect(shortcodes[req.params.shortcode])
}else{
let url = await commands.get("query").function("SELECT dtOriginal from tbl_urls WHERE idShortcode = ?",[req.params.shortcode])
if(url.length == 0)
return
shortcodes[req.params.shortcode] = url[0].dtOriginal
res.redirect(shortcodes[req.params.shortcode])
}
// commands.get("query").function("UPDATE tbl_urls SET dtAccessed = dtAccessed + 1 WHERE idShortcode = ?",[req.params.shortcode])
let ipHash = req.headers["cf-connecting-ip"] == undefined ? "localhost" : crypto.createHash('md5').update(req.headers["cf-connecting-ip"]).digest('hex')
commands.get("query").function("INSERT INTO tbl_access (fiShortcode,dtIpHash,dtTimestamp) VALUES (?,?,?)",[req.params.shortcode, ipHash, new Date().getTime()])
}
}
+42
View File
@@ -0,0 +1,42 @@
module.exports = {
name:"Startup_function",
async function(msg, args){
setTimeout(async () => {
console.log("-------------------------------- Startup Function ------------------------------");
// Load all shortcodes with their redirects directly into memeory on startup
process.stdout.write("Loading all shortcodes into memory 🟥");
let url = await commands.get("query").function("SELECT idShortcode, dtOriginal from tbl_urls",[])
for (let i = 0; i < url.length; i++) {
shortcodes[url[i].idShortcode] = url[i].dtOriginal
}
process.stdout.cursorTo(40)
process.stdout.write(`🟩 (${url.length})\n`)
// Load all bans into memory
process.stdout.write("Loading all bans into memory 🟥");
let bans = await commands.get("query").function("SELECT dtIpHash from tbl_bans",[])
for (let i = 0; i < bans.length; i++) {
ignorelist[bans[i].dtIpHash] = bans[i].dtIpHash
}
process.stdout.cursorTo(40)
process.stdout.write(`🟩 (${bans.length})\n`)
// Ratelimit hit cleanup
if(config.ratelimit.resetHits){
setInterval(() => {
for(hash in ratelimit){
if(ratelimit[hash][1] > 0)
ratelimit[hash][1] = ratelimit[hash][1]-1
}
}, 60000* config.ratelimit.resetTimeframe); // Timeframe value in minutes
}
console.log("--------------------------------------------------------------------------------");
}, 500);
}
}
+30
View File
@@ -0,0 +1,30 @@
const crypto = require('crypto');
module.exports = {
name:"banHandler",
check: async function(req){
return new Promise(async (resolve,reject) => {
let hash = req.headers["cf-connecting-ip"] == undefined ? crypto.createHash('md5').update("localhost").digest('hex') : crypto.createHash('md5').update(req.headers["cf-connecting-ip"]).digest('hex')
let ban = await commands.get("query").function("SELECT idBan from tbl_bans WHERE dtIpHash = ?",[hash])
if(ban.length == 0){
resolve()
return
}else{
reject()
return
}
})
},
ban: async function(req){
if(req.headers["cf-connecting-ip"] != undefined){
if(config.banhandler.whitelisted.includes(req.headers["cf-connecting-ip"]))
return
}
let hash = req.headers["cf-connecting-ip"] == undefined ? crypto.createHash('md5').update("localhost").digest('hex') : crypto.createHash('md5').update(req.headers["cf-connecting-ip"]).digest('hex')
await commands.get("query").function("INSERT INTO tbl_bans (dtIpHash,dtTimestamp) VALUES (?,?)",[hash, new Date().getTime()])
ignorelist[hash] = hash
},
unban: async function(hash){
await commands.get("query").function("DELETE FROM tbl_bans WHERE dtIpHash = ?",[hash])
delete ignorelist[hash]
}
}
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
name:"log",
async function(message,status){
switch(status){
case "good":
console.log(`${new Date()} : ${message}`.green)
break;
case "warn":
console.log(`${new Date()} : ${message}`.yellow)
break;
case "bad":
console.log(`${new Date()} : ${message}`.red)
break;
}
}
}
+47
View File
@@ -0,0 +1,47 @@
// const included = require("../../requires")
pool = mysql.createPool({
connectionLimit : 10,
host: config.database.host,
port: config.database.port,
user: config.database.username,
password: config.database.password,
database: config.database.database,
charset : 'utf8mb4'
});
module.exports = {
name: "query",
async function(query, values, verbose){
// if(values == undefined){
// values = query
// query = msg
// }
return new Promise((resolve, reject) => {
// connection.query(query, values, function (error, results, fields) {
// if (error) throw error;
// resolve(results)
// });
pool.getConnection(function(err, connection) {
if (err) throw err; // not connected!
// Use the connection
connection.query(query, values, function (error, results, fields) {
// When done with the connection, release it.
connection.release();
// Handle error after the release.
if (error){
// console.log(error) //-------------------------------------------------------------------------- DEBUGGING
if(verbose){
reject(error.sqlMessage);
}
resolve(false)
}
resolve(results)
// Don't use the connection here, it has been returned to the pool.
});
});
})
}
}
+48
View File
@@ -0,0 +1,48 @@
const crypto = require('crypto');
module.exports = {
name:"ratelimit",
async function(req){
return new Promise(async (resolve,reject) => {
// If developer key has been sent, skip ratelimiter
if(req.headers.devkey){if(req.headers.devkey == config.ratelimit.devKey){resolve();return}}
// If user is banned already, reject with code 3 (ignore)
let bh = false
await commands.get("banHandler").check(req).catch(err => {reject(3);bh=true})
if(bh){return}
if(!config.ratelimit.enabled){
resolve()
return
}
let hash = req.headers["cf-connecting-ip"] == undefined ? crypto.createHash('md5').update("localhost").digest('hex') : crypto.createHash('md5').update(req.headers["cf-connecting-ip"]).digest('hex')
if(ratelimit[hash] == undefined){
// Everything is fine
ratelimit[hash] = [new Date().getTime()+1000,0]
resolve()
return
}
else{
if(ratelimit[hash][0] > new Date().getTime()){
ratelimit[hash][1] = ratelimit[hash][1] + 1
if(ratelimit[hash][1] >= config.ratelimit.hitsUntilBan){
// User gets banned now, reject with code 2 (tell user about the ban)
commands.get("banHandler").ban(req)
reject(2)
console.log(`${new Date()}: Banning ${hash}`);
return
}
// User hits ratelimit, reject with code 1 (tell user about the hit)
reject(1)
// console.log(`Rate Limiting ${hash}`);
return
}else{
// Everything is fine
ratelimit[hash][0] = new Date().getTime()+1000
resolve()
return
}
}
})
}
}
+38
View File
@@ -0,0 +1,38 @@
module.exports = {
name:"readableTime",
async timeframe(seconds){
return new Promise(resolve => {
seconds = Math.floor(Number(seconds)/1000);
var d = Math.floor(seconds / (3600*24));
var h = Math.floor(seconds % (3600*24) / 3600);
var m = Math.floor(seconds % 3600 / 60);
var s = Math.floor(seconds % 60);
var dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : "";
var hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
var mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
var sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
resolve(dDisplay + hDisplay + mDisplay + sDisplay);
})
},
async timestamp(timestamp=new Date()){
return new Promise(resolve => {
let vals = {
yyyy: timestamp.getFullYear(),
m: timestamp.getMonth()+1,
d: timestamp.getDate(),
h: timestamp.getHours(),
mi: timestamp.getMinutes(),
s: timestamp.getSeconds(),
ms: timestamp.getMilliseconds()
};
// console.log(`${vals.yyyy}/${vals.m}/${vals.d} ${vals.h}:${vals.mi}:${vals.s}`);
resolve(`${vals.yyyy}/${vals.m}/${vals.d} ${vals.h}:${vals.mi}:${vals.s}`)
})
}
}
+71
View File
@@ -0,0 +1,71 @@
// require("../../requires")
module.exports = {
name: "registerCommands",
async function(global, guild){
return new Promise(resolve => {
const { SlashCommandBuilder } = require('@discordjs/builders');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v9');
const token = config.token
const commandsList = []
var commandstopush = "| "
commands.forEach(element => {
if(element.slashcommand != undefined){
if(global){
if(element.slashcommandGlobal){
commandstopush += `${element.name} | `
commandsList.push(element.slashcommand)
}
}else{
commandstopush += `${element.name} | `
commandsList.push(element.slashcommand)
if(element.permissions){
commandsList[commandsList.length-1].default_member_permissions = element.permissions
}
}
// commandsList.push(element.slashcommand)
}
});
console.log(commandstopush);
const rest = new REST({ version: '9' }).setToken(token);
const clientId = client.user.id;
const guildId = '929749429156724807';
(async () => {
try {
if(!global){
await rest.put(
Routes.applicationGuildCommands(clientId, guildId),
{ body: commandsList },
);
}else{
if(guild == undefined){
await rest.put(
Routes.applicationCommands(clientId),
{ body: commandsList },
);
}else{
await rest.put(
Routes.applicationGuildCommands(clientId, guild.id),
{ body: commandsList },
);
}
}
console.log('Successfully registered application commands.');
resolve()
} catch (error) {
console.error(error);
resolve()
}
})();
})
}
}
+29
View File
@@ -0,0 +1,29 @@
const included = require("../../requires")
// Here you can add functions that will be run at each startup of the bot
module.exports = {
name:"reloadCommands",
async function(msg, args){
commands.clear()
var path = `${__dirname}/../../functions`
var folders = fs.readdirSync(path).filter(function (file) {
return fs.statSync(path+'/'+file).isDirectory();
});
folders.forEach(element => {
var commandFiles = fs.readdirSync(`${path}/${element}`).filter(file => file.endsWith('.js') && !file.startsWith("index"));
for (const file of commandFiles) {
delete require.cache[require.resolve(`${path}/${element}/${file}`)];
try {
const command = require(`${path}/${element}/${file}`);
commands.set(command.name, command);
} catch (error) {
console.log(error);
}
}
});
// commands.get("garbage_collector").function() // reset garbage collector
console.log(`Reloading ${commands.size} modules done`)
}
}
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
name:"validate",
check: async function(apiKey){
return new Promise(async (resolve,reject) => {
if(apiKey == undefined){
reject(1)
}
let user = await commands.get("query").function("SELECT idUser from tbl_users WHERE dtApiKey = ?",[apiKey])
if(user.length == 1){
resolve(user[0].idUser)
}else{
// resolve(false)
reject(2)
}
})
}
}
+258
View File
@@ -0,0 +1,258 @@
/*
⣷⠄⠄⠘⡄⠘⡀⠹⣿⠛⢿⣏⠈⢿⠈⠻⣧⠈⢿⠋⠉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠄⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠁⠄⢀⣾⣿⣿⣿⣿⣿⣿⡟⠁⣼⡇⠄⣿⣿⠄⢸⠇⢠⠇⢰⠁⠄⠄⣿⡿⠄⣿⣿⡿⠄⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⡀⠄⠄⢳⡀⠐⡀⠹⣧⠈⢿⣇⠈⢧⠄⠹⣧⠄⠄⠄⠄⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠄⠄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⠁⠄⠄⣠⣶⣿⣿⣿⣿⣿⣿⣿⡟⠄⠐⣿⡇⠄⣿⡏⠄⣼⠄⢸⠄⢸⠄⠄⢰⣿⡇⠄⠃⢸⡇⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣇⠄⠄⠈⣧⠄⢱⠄⢻⡆⠈⣿⣆⠈⢧⠄⢹⣧⠄⠄⠄⠄⠄⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠄⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⠄⠄⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⠟⠄⣰⠄⢸⡇⢸⣿⡇⠄⣿⠄⡀⠄⡄⠄⠄⢸⣿⠁⠄⠄⣿⡇⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⠄⠄⠄⢻⣇⠄⢧⠄⢻⡀⠸⣿⡆⠈⣧⠄⢻⣧⠄⠄⠘⣦⠄⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠄⠄⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠄⠄⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠄⣰⣿⡇⢸⡇⢸⣿⠄⠄⡇⠄⡇⠄⡇⠄⠄⢸⡿⠄⠄⢠⣿⠄⠄⠄⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣇⠄⠄⠈⢿⡄⠘⣇⠄⢧⠄⢿⣿⡄⠘⣧⠄⢿⣷⣶⡀⠸⣧⡀⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠄⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠄⠄⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠄⠄⢻⣿⠄⢸⡇⢸⣿⡆⢸⡇⢀⡇⠄⠁⠄⠄⢸⡇⠄⠄⢸⣿⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⡀⠄⠄⠄⢳⠄⢹⣆⠈⣧⣸⡿⠟⠄⠉⠁⠄⣉⣿⣿⣤⣿⣷⡀⠄⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠄⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⠄⠄⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠄⣰⡀⠈⠟⠁⢀⣀⣀⣀⣀⣀⠄⠈⠃⠄⠄⠄⠄⢸⠃⠄⠄⣿⡟⠄⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⡇⠄⠄⠄⠄⢣⠄⢻⡄⠈⠁⠄⠠⠶⢶⣾⣿⣿⣿⣿⣿⣿⣿⣿⡄⠄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⠄⠹⠿⠿⠿⠛⠛⠛⠛⠛⠛⠿⢿⣿⣿⡿⠁⠄⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠄⣰⣿⣿⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⠄⢰⠄⢸⠄⠄⠄⣿⡇⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⠄⠄⠄⢳⠄⢂⠈⣷⣄⣤⣤⠄⠄⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣀⠄⠄⠄⠄⢀⣀⣀⡀⠄⠄⠄⠈⠉⠄⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⣄⡀⠘⠄⢸⠄⡇⠄⣿⠇⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⡆⠄⠄⠘⣧⠈⢦⡘⣿⡿⠃⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣾⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣤⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠛⠒⠄⣿⣿⡇⠄⣿⠄⠄⠄⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⠄⠄⠄⠻⣧⠈⣿⡿⠁⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠹⣿⡇⢹⡇⢸⡟⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⡇⠄⠄⠄⠹⣆⠘⠃⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠘⡇⢸⡇⢸⡇⠄⠄⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⢻⣆⠄⠘⠋⠉⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣄⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠛⠛⠛⣻⣿⣿⣿⣿⣿⣿⡿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠄⠄⢸⡇⠈⠁⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣇⠄⠄⠰⡀⢻⣦⠄⠄⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠈⣿⣿⣿⣿⣿⣿⡿⠟⠛⠉⢀⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⢀⣤⣾⣿⣿⣿⣿⡿⠿⢿⠿⠻⠿⠿⠿⠿⣿⣿⣿⣿⡿⠄⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠄⢸⡇⠄⠄⠄⠄⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡀⠄⠄⣷⣤⣿⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⠿⠛⠋⠁⠄⢀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠄⠄⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⠄⠄⢄⡀⠄⢤⣀⣉⡙⠛⠁⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠋⠉⠙⡇⠄⡇⠄⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣧⠄⠄⢸⣿⡇⠄⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠇⠄⠋⠁⠄⠄⢀⣤⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⢀⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠙⠄⠄⠻⣿⣿⣷⣶⣿⣏⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⢀⠄⢸⡇⠄⡇⠄⣇⠄⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡄⠄⠄⣿⠁⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠄⠄⣠⠔⠁⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣾⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠄⠄⠘⢿⣿⣿⣿⣿⣿⣶⣌⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠄⣾⠄⢸⣷⠄⣿⠄⢿⠄⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣧⠄⠄⢹⠄⠘⣿⣿⣿⣿⣿⣿⣿⡿⠁⡀⠄⠄⣠⡾⠃⢠⣾⣿⣿⣿⣿⣿⣿⣿⡿⠛⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠄⠈⢿⣿⣿⣿⣿⣿⣿⣷⣄⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⢹⠄⣸⣿⠄⣿⠄⢸⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠄⠘⣷⠖⢿⣿⣿⣿⣿⣿⣿⠁⣼⠃⢀⣾⡟⠁⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⢰⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠄⠻⣿⣿⣿⣿⣿⣿⣿⣦⠈⠉⠙⠿⣿⣿⣿⡿⠁⠈⠉⠄⠄⣻⣿⣿⣿⣤⣿⠄⢸⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠄⠄⢿⣆⠈⢻⣿⣿⠏⡉⠁⠄⠃⢀⣾⠋⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⢀⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠄⠙⢿⣿⣿⣿⣿⣿⣿⣧⢀⡀⠄⡈⠙⠋⠄⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣾⠄⠄⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠄⢸⣿⣆⠄⠛⠃⢀⣿⣿⠆⠄⡼⠁⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠄⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⠄⠸⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⡟⢻⣿⣿⣿⣿⣿⣿⣿⣿⣦⣠⣬⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠄⠘⣿⣿⣷⣶⣾⣿⣿⡟⠄⠄⠄⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⣿⣿⣿⡏⠄⣿⣿⣿⣇⠄⠁⠄⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⡇⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⠁⠄⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣼⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣦⡀⠄⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣧⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠄⠄⢠⣿⣿⣿⣿⣿⣿⡇⠄⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⢠⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣶⣿⣿⣿⣿⣿⣿⣿⠄⢿⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠄⠄⣿⣿⣿⣿⣿⣿⣿⠄⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⡆⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠄⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠄⢀⣿⣿⣿⣿⣿⣿⣿⣀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⢸⣿⣿⠟⢿⣿⣿⣿⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⣿⣿⣿⠄⢸⣿⣿⡇⠄⢿⣿⣿⣿⣿⣿⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⠄⠄⠄⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠄⠄⣿⣿⠃⠄⠄⢻⣿⣿⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⣿⣿⣿⠄⠄⢻⣿⡇⠄⠘⣿⣿⣿⣿⣿⡀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠄⠄⠄⣿⡟⠄⠄⡀⠈⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢹⣿⣿⠄⠄⠈⣿⡇⠄⡄⠘⣿⣿⣿⣿⡇⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⡀⠄⡄⠄⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠄⠄⡄⠄⣿⠃⠄⢸⣧⠄⠸⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⠄⠄⠄⢹⡇⠄⣿⡄⠘⣿⣿⣿⡇⠄⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⠄⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⠄⣼⡇⢰⡏⠄⠄⣿⣿⡆⠄⢿⡇⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⠄⢸⡄⠈⠇⢀⣿⣷⠄⠸⣿⣿⣿⠄⣄⠄⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⡇⠄⠄⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡏⠄⠄⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⠄⢰⣿⡇⠈⠁⠄⣸⣿⣿⣷⠄⠘⠁⠄⢻⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⠄⢸⣷⠄⠄⢸⣿⣿⣧⠄⠹⣿⣿⠄⢹⡀⠘⢿⣿⣿⣿⣿⣿⡟⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠄⠄⠄⣿⣿⡇⠄⠄⠄⣿⣿⣿⣿⡆⠄⠄⡄⠈⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⠄⢸⣿⣧⡀⢸⣿⣿⣿⣇⠄⢻⣿⡇⢸⣇⠄⠘⣿⣿⡿⠛⠁⡀⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⡇⠄⠉⠻⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⠄⠄⠄⢸⣿⣿⡇⠄⠄⢸⣿⣿⣿⣿⣿⠄⠄⣿⠄⢹⣿⣿⣿⣿⣿⣿⣿⡄⢸⣿⣿⠄⢸⣿⣿⣧⣸⣿⣿⣿⣿⣆⠄⢿⠇⠈⢻⡀⠄⠙⠁⣀⣴⣾⣧⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⣾⣿⣿⣿⣿⣿⣿⠁⢠⣦⣄⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣯⠄⠄⣸⣿⣿⣿⣿⣿⣿⣿⠋⢻⣿⣿⣿⣿⣿⣿⡿⠄⠄⠄⠘⢿⣿⡇⠄⠄⢸⣿⣿⣿⣿⣿⡇⣰⣿⡇⠸⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠄⠄⠠⠄⠁⠄⠄⢸⣿⣿⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⣿⣿⣿⣿⣿⣿⣿⠄⠄⠈⠙⠃⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣗⠄⠄⣿⣿⣿⣿⣿⣿⣿⡏⠄⣸⣿⣿⣿⣿⣿⣿⠇⢠⣶⣶⣤⡀⠈⠃⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠘⣿⣿⣿⣿⣿⣿⡗⠈⣿⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠓⠄⣀⣤⠄⠄⠄⠄⢿⣿⣿⣿⡄⢸⣿⣿⣿⣿⣿⣿⣿⣿⡏⠄⣿⣿⣿⣿⣿⣿⡟⠄⠸⣷⠦⠄⠄⠄⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⡏⠄⣾⣿⣿⣿⣿⣷⣤⡀⠄⠙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣇⠄⠈⢿⣿⣿⣿⣿⣧⠄⣿⡟⠄⢸⣿⣿⣿⣿⣿⣿⠿⠋⠁⣠⣴⣾⣿⣿⣇⢰⣄⠄⠸⣿⣿⣿⣇⠄⢻⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⡇⠄⢠⣀⣠⡀⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⣿⣿⣿⣿⣿⣿⣿⠁⢠⣿⣿⣿⣿⣿⡿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⡀⣤⣀⠄⠙⢿⣿⣿⣿⣿⣿⣿⣦⠄⠘⣿⣿⣿⣿⣿⠄⢹⠃⠄⣼⣿⣿⣿⠛⠉⣀⣤⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠄⢿⣿⣿⣿⡄⠈⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣷⠄⠾⠟⠋⠁⠄⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠄⣿⣿⣿⣿⣿⣿⡟⠄⣸⣿⣿⣿⣿⣿⠇⠄⠛⠛⠻⠿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣀⣽⣿⣿⣿⣿⣿⣿⣇⠄⠸⣿⣿⣿⣿⠄⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠄⠘⠿⠿⣿⣷⠄⢹⣿⣿⣿⣿⣿⡿⠃⢸⣿⣿⣿⣿⣿⣿⡿⠄⣤⣤⣶⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⢹⣿⣿⡿⢿⣿⠃⢀⣿⣿⣿⣿⣿⡿⠄⣀⠄⠄⠄⠄⠄⠉⠛⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠄⠙⣿⣿⣿⠄⢠⣤⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠛⠉⠄⠄⠄⠄⠄⢀⡀⠈⣿⣿⣿⣿⣿⡇⠄⢸⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⢸⣿⣿⡇⠈⣿⠄⢸⣿⣿⣿⣿⣿⡇⠄⣿⣿⣶⣤⣀⠄⠄⠄⠄⠄⠈⠙⠛⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠘⢿⣿⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠟⠛⠉⠄⠄⠄⠄⠄⠄⢀⡀⠄⠄⣶⣿⣷⠄⢻⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⣿⣿⡇⠄⡏⠄⢸⣿⣿⣿⣿⣿⠁⢸⣿⣿⣿⣿⣿⣿⣷⣦⣤⣀⡀⠄⠄⠄⠄⠉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠄⠙⠂⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⠄⠄⠄⠄⠄⠄⣀⣤⣶⣾⣿⣿⣷⡀⠄⢻⣿⣿⡄⠘⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣿⠃⠄⣿⣿⣿⡇⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠄⠄⢻⣿⡇⠄⣿⠄⢸⣿⣿⣿⣿⡿⠄⣸⣿⣿⣿⣿⣿⣿⠿⠛⠉⠁⠄⠄⠄⠄⠄⢀⣀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⣈⣿⣿⣿⣿⣿⣿⣿⣿⣏⠄⠄⠄⠄⠄⠘⠻⣿⣿⣿⣿⣿⣿⣿⣷⠄⠘⣿⣿⣧⠄⢿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⣿⣿⠄⢰⣿⣿⣿⡇⠄⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠄⢸⣿⡇⠄⢸⠄⢸⣿⣿⣿⣿⡇⠄⣿⣿⡿⠟⠋⠁⠄⠄⠄⠄⠄⣀⣠⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⣀⠄⠄⠄⠄⠄⠉⠉⠛⠛⠿⢿⣿⡇⠄⢿⣿⣿⡀⢸⣿⣿⣿⡇⠄⢻⣿⣿⣿⣿⣿⣿⠄⢸⣿⣿⣿⣇⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠄⠸⣿⣿⠄⢸⠄⢸⣿⣿⣿⣿⠃⢰⣿⠁⠄⠄⠄⠄⣀⣤⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣀⡀⠄⠄⠄⠄⠄⠈⠁⠄⠸⣿⣿⣇⠄⠻⣿⣿⡇⠄⢸⣿⣿⣿⣿⣿⣿⠄⣼⣿⣿⣿⣿⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠄⢻⣿⠄⠈⠄⢸⣿⣿⣿⡿⠄⣼⣿⡷⠄⠒⢸⠏⠉⣿⠟⢹⠏⠉⠟⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠋⠉⠛⠛⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⠙⡿⠛⠋⠹⠓⡢⠄⠄⡀⠄⡀⠄⢸⣿⣿⡀⠄⣿⣿⡇⠄⢸⣿⣿⣿⣿⣿⡿⠄⣿⣿⣿⣿⣿⡄⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠄⠈⢿⡆⠄⠄⠸⣿⣿⣿⡇⠄⠄⡟⠁⠄⢀⡞⠄⣰⠃⠄⠄⠄⠄⢀⣾⣿⣿⣿⣿⣿⣿⠟⢁⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⠻⣿⣿⣿⠟⠁⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⢀⣼⠇⠄⠄⢻⣿⡇⠄⣿⣿⡇⠄⢸⣿⣿⣿⣿⡿⠁⢰⣿⣿⣿⣿⣿⡇⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠄⠘⢿⡀⠄⠄⣿⣿⣿⠁⠄⡜⠄⠄⢀⡞⠄⡰⠁⠄⠄⠄⠄⢠⣾⣿⣿⣿⣿⣿⣿⠇⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠘⣿⣿⠄⠄⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⡰⠊⠄⠄⠄⠄⠄⢿⡇⠄⣿⣿⡇⠄⢸⣿⣿⣿⠟⠄⠄⢸⡿⠛⣿⣿⣿⣧⠄⠄⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⠄⠄⠄⠁⠄⠄⢸⣿⣿⠄⠘⠄⠄⢀⡞⢀⣴⠃⢀⣤⣴⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢻⣿⣤⣾⣥⡀⣀⠄⣠⠂⠄⠄⠄⡠⠊⠄⠄⠄⢀⣴⠄⠄⢸⡇⠄⣿⣿⡇⠄⢸⡿⠃⠄⣴⡇⠄⠈⢀⣼⣿⣿⣿⣿⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠄⠄⠄⠄⠄⠄⠄⠄⢿⣿⠄⢰⣾⣶⣿⣷⣾⣿⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠈⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣎⣀⡄⠄⣠⣶⣿⣿⡆⠄⠄⠇⢠⣿⣿⣧⠄⠈⢀⡄⠄⢻⡇⠄⠄⣾⣿⣿⣿⣿⣿⡆⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⠄⣸⡇⠄⠄⠄⠄⠉⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠄⠄⢀⡀⠈⠄⣀⠄⣾⣿⣷⠄⢸⡇⠄⠄⡿⢿⣿⣿⣿⣿⡇⠄⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠄⠄⢠⣿⠄⠄⣿⣿⣶⣿⣇⠄⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣀⣀⣀⣤⣤⣀⡀⠄⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠄⢸⣿⣿⣿⣿⠄⠹⠋⣩⠄⢸⣧⠄⠄⢀⣾⣿⣿⣿⣿⣿⠄⠄⠘⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⣸⣿⠄⠄⣿⣌⠛⢿⣿⠄⠄⠄⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⠄⠄⠄⠄⠄⢀⣠⣶⣾⣿⣿⣿⣿⣿⣿⣿⣷⣆⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⠁⠄⣼⣿⣿⣿⣿⠄⢰⣿⣿⡄⠈⡏⠄⠄⢾⣿⣿⣿⣿⣿⣿⡆⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⠄⣿⡟⠄⠄⣿⣿⣷⣦⣽⡄⠄⣿⠄⡀⠉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⠄⠄⠄⠄⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⠉⢀⡤⠄⠄⠄⠄⣿⣿⣿⣿⣿⡇⠸⣿⡟⣁⠄⠛⠄⠄⠸⠋⢸⣿⣿⣿⣿⣷⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⠄⠄⠄⢰⣿⡇⠄⠄⣉⣉⣉⠙⣻⡇⠄⠹⠄⢻⠄⢀⠈⠙⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠸⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠁⠄⠄⠄⠄⢀⡴⠇⠄⠄⣿⣿⣿⣿⣿⡇⠄⣿⣾⡿⠃⠄⠄⠄⠄⣠⣿⣿⣿⣿⣿⣿⡆⠄⠄⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠄⠄⠄⣼⣿⡇⠄⠄⣿⣿⣿⣿⣿⡇⠄⠄⠄⢸⠄⢸⣷⣦⣄⡈⠉⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠄⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⣿⣿⣿⣿⡿⠟⠋⠁⠄⠄⠄⠄⠄⠄⠋⠄⡀⠄⠄⣿⣿⣿⣿⣿⡇⠄⣿⠏⢀⣴⣆⠄⠄⠄⢿⠟⢉⣿⣿⣿⣿⣿⡀⠄⠹⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡿⠄⠄⠄⢀⣿⣿⡇⠄⠄⣿⣿⣿⣿⣿⡇⠄⠄⠄⢸⠄⢸⣿⣿⣿⣿⣿⣶⠄⠄⠈⠛⠿⢿⣿⣿⣿⣧⠄⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⣿⡿⠿⠋⠁⠄⠄⣠⣿⠁⠄⠄⠄⠄⠄⠰⠋⠄⠄⠄⣿⣿⣿⣿⣿⡇⠄⢁⣴⣿⣿⡿⠄⠄⠄⠄⣠⣾⣿⣿⣿⣿⣿⣧⠄⠄⠙⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⠄⠄⣸⣿⠟⠋⠄⠄⢸⣿⣿⣿⣿⡇⠄⠄⠄⢸⠄⣾⣿⠟⢉⣴⠟⢉⠄⠄⠄⠄⣤⣄⣀⠉⠉⠛⠄⠄⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠛⠛⠋⠁⠄⠄⠄⣀⣤⣴⣾⡿⠋⠁⠄⠄⠄⠄⠄⠄⢠⣶⡇⠄⠄⣿⣿⣿⣿⣿⡇⠄⣿⣿⠟⠉⣀⡆⠄⠄⠄⣿⠟⣻⣿⣿⣿⣿⣿⣆⠄⠄⢹⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⢠⣿⠏⠄⡰⠄⠄⢸⣿⣿⣿⣿⡇⠄⠄⠄⣾⡴⠛⢁⣴⠟⠁⣠⣿⠄⠄⠄⠄⢸⡿⠋⢀⣴⡿⠖⣤⣤⡄⣀⣀⡀⠄⠄⠉⠉⠉⠁⠄⠄⠄⠄⣤⣶⣶⣿⣿⡇⠄⢹⣿⣿⠟⠁⠄⢀⣤⠄⠄⠄⠄⠄⠘⠋⠄⠄⢸⣿⣿⣿⣿⣿⡇⠄⡟⠁⣠⣾⠿⣣⡀⠄⠄⠄⣴⡿⠻⣿⣿⣿⣿⣿⡄⠄⠈⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡿⠄⠄⠄⣼⣿⠄⠄⢁⠄⠄⢸⣿⣿⣿⣿⡇⠄⢠⡠⠋⣀⣴⠟⠁⣠⣾⡿⠟⠄⠄⠄⠄⠄⢀⣴⡿⠋⢀⣴⠟⠁⣠⣿⣿⡇⠄⠄⠄⠄⠄⣀⣀⣤⣶⣿⣿⣿⣿⣿⣿⡇⠄⢸⠟⠁⠄⢀⣴⠟⠁⡀⠄⠄⠄⠄⢠⡴⠃⠄⢸⣿⣿⣿⣿⣿⡇⠄⣷⡿⠋⣡⣾⣿⡇⠄⠄⢸⠟⢀⣼⣿⣿⣿⣿⣿⣧⠄⠄⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⠇⠄⠄⢰⣿⠏⢀⡴⠛⠄⠄⢻⣿⣿⣿⣿⡿⠄⢈⣠⣾⡟⠁⣠⣾⡿⠋⢀⣴⠄⠄⠄⠄⣾⡿⠋⢀⣴⠟⠁⣠⣾⣿⣿⣿⣷⠄⠄⠄⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠄⠰⡞⢀⣴⠟⠁⡠⠊⠄⠄⠄⠄⠄⢸⣤⣤⠄⣿⣿⣿⣿⣿⣿⡇⠄⢋⣠⣾⣿⡿⠋⣀⠄⠄⠄⣠⣾⣿⣿⣿⣿⣿⣿⣿⡄⠄⠄⣿⣿⣿
⣿⣿⣿⣿⣿⣿⡿⠄⠄⢠⣿⣿⣾⠟⢀⣴⡇⠄⠸⣿⣿⣿⣿⡇⠄⣸⣿⠋⣠⣾⡿⠋⢀⣤⡿⠿⡇⠄⠄⠄⠋⠄⣰⠟⠁⣠⣾⣿⣿⡿⠟⠛⠁⠄⠄⠄⢸⣿⣿⣿⣿⣿⣿⣿⡿⠛⠁⣠⣶⡄⠄⡀⠙⠳⢤⡞⠄⣠⠞⠁⠄⠄⠄⠄⢸⣿⠄⢸⣿⣿⣿⣿⣿⡇⢰⣿⣿⡿⠋⣠⣾⣿⠄⠄⠄⠹⠋⣻⣿⣿⣿⣿⣿⣿⣧⠄⠄⢸⣿⣿
⣿⣿⣿⣿⣿⣿⡇⠄⢀⣾⣿⣿⠋⣠⡾⠋⣡⠄⢀⣀⣀⠄⠈⠄⠄⢻⣿⣾⡿⠋⢀⣴⡿⠋⣠⣾⡇⠄⠄⢠⣴⠞⠁⣠⣾⣿⠿⠛⠉⠄⣠⣴⣿⣿⡆⠄⣾⣿⣿⣿⣿⣿⡿⠉⢀⣠⣾⣿⣿⣇⣼⣿⠆⣤⣀⠈⠙⠁⢠⡶⠄⠄⠄⢀⣾⣿⠄⠘⣿⣿⣿⣿⣿⠇⢸⡿⠋⣠⣾⣿⠟⣿⡆⠄⠄⠄⣾⣿⣿⣿⣿⣿⣿⣿⣿⡄⠄⠄⣿⣿
⣿⣿⣿⣿⣿⣿⠄⢠⣿⣿⣿⣧⡾⠋⣠⣾⣿⠄⣼⣿⣿⣿⣿⡇⠄⣿⣿⠋⢀⣴⡿⠋⢀⣾⣿⠟⢻⡀⠄⠸⠃⣠⣾⡿⠋⢀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣶⣿⣿⣿⣿⣿⣟⣁⣴⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣷⣦⣄⣀⠘⠋⠄⣈⠉⠙⠄⢀⡈⠙⠛⠉⣹⠄⣋⣤⣾⣿⢟⣡⣾⣿⣷⣄⢠⣤⣿⣉⣸⣿⣿⣿⣿⣿⣿⣷⠄⠄⢸⣿
*/
const start = new Date()
console.log(start);
require("./requires")
// loading functions
commands = new Map()
var path = `${__dirname}/functions`
var folders = fs.readdirSync(path).filter(function (file) {
return fs.statSync(path+'/'+file).isDirectory();
});
folders.forEach(element => {
var commandFiles = fs.readdirSync(`${__dirname}/functions/${element}`).filter(file => file.endsWith('.js') && !file.startsWith("index"));
for (const file of commandFiles) {
const command = require(`./functions/${element}/${file}`);
commands.set(command.name, command);
}
});
// General stuff
var platform = process.platform;
// Express
const express = require("express");
const app = express()
const port = 3777
app.use(bodyParser.urlencoded({
extended:true
}));
// For monitoring
app.get('/api/v1/health', async (req, res) => {
if(await ratelimiter(req, res))return
res.json({
response:"okay",
startup:await commands.get("readableTime").timestamp(start),
uptime:await commands.get("readableTime").timeframe(new Date().getTime() - start.getTime())
})
})
// Functionality
/*
// This is the code used for the rate limiter and ban system
// If you need to do changes, do them here
*/
async function ratelimiter(req, res){
return new Promise(async (resolve) => {
let rl = false;
await commands.get("ratelimit").function(req).catch(err => {
rl = true;
switch(err){
case 1: res.json({code:1});return
case 2: res.json({code:2});return
case 3: return
};
})
resolve(rl);
})
}
// As for the banhandler and ratelimiter, i wasnt able to come up with a better way to implement it here so that i wont need all these repeating lines
// If you have a better way to do it, feel free to tell me
// GET
app.get('/api/v1/info/:shortcode', async (req, res) => {
// Endpoint to get information about a shortcode
if(await ratelimiter(req, res))return
commands.get("info").function(req, res)
})
app.get('/api/v1/codeList', async (req, res) => {
// Endpoint to get information about a shortcode
if(await ratelimiter(req, res))return
commands.get("codeList").function(req, res)
})
app.get('/api/v1/test', async (req, res) => {
// Testing endpoint uwu
if(await ratelimiter(req, res))return
res.status(418)
res.send("I am a teapot, leave me alone")
})
// POST
app.post('/api/v1/createUser', async (req, res) => {
// Endpoint to create user account
if(await ratelimiter(req, res))return
commands.get("createUser").function(req, res)
})
app.post('/api/v1/authenticate', async (req, res) => {
// Endpoint to create API key
if(await ratelimiter(req, res))return
commands.get("authenticate").function(req, res)
})
app.post('/api/v1/createCode', async (req, res) => {
// Endpoint to create API key
if(await ratelimiter(req, res))return
commands.get("createCode").function(req, res)
})
// DELETE
app.delete('/api/v1/deleteCode', async (req, res) => {
// Endpoint to delete shortcodes
if(await ratelimiter(req, res))return
commands.get("deleteCode").function(req, res)
})
// Main Page Handler
app.use(express.static(__dirname+"/webui"))
app.use(express.static(__dirname+"/webui/favicon.ico"))
app.use(express.static(__dirname+"/webui/stuff"))
app.use(express.static(__dirname+"/webui/pages"))
// REDIRECTION HANDLER
app.get('/:shortcode', async (req, res) => {
// Endpoint to get information about a shortcode
if(await ratelimiter(req, res))return
if(!(req.params.shortcode.match(/^\w{3,18}$/))){
console.log(req.params.shortcode + " Did not match");
return
}
commands.get("redirect").function(req, res)
})
app.listen(port, () => {
commands.get("Startup_function").function()
console.log(`------------------------------------ Status ------------------------------------ \n` +
`${__dirname} \n` +
`${platform} \n` +
`Listening on port ${port} \n` +
`The Startup took ${new Date() - start}ms \n` +
`${commands.size} modules loaded`);
})
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on("line", data =>{
const args = data.trim().split(" ");
const command = args.shift().toLowerCase();
switch(command){
case "exit":
process.exit(1);
break;
case "reload":
commands.get("reloadCommands").function()
break;
default:
console.log("This is not a recognised command");
break;
};
})
// catch uncaught exceptions to prevent process crashes on random bugs
process.on("uncaughtException", (err) => {
// console.log(process.stderr);
console.error(err.stack);
// console.error(err.stack.length);
// try{
// commands.get("queryCommand").function("INSERT INTO tbl_errorlog (dtError) VALUES (?)",[err.stack])
// }catch(error){
// console.log(error);
// }
})
+2278
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
{
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"colors": "^1.4.0",
"express": "^4.18.2",
"hashids": "^2.3.0",
"mysql": "^2.18.1"
},
"name": "url_shortener",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "node main",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "🌺Emily 🍞/🥖🌸",
"license": "ISC",
"description": ""
}
+21
View File
@@ -0,0 +1,21 @@
config = require("./config/main.json")
readline = require("readline")
mysql = require("mysql")
fs = require("fs")
colors = require("colors");
express = require('express')
bodyParser = require("body-parser")
bcrypt = require("bcrypt")
Hashids = require("hashids/cjs")
// - Global Variables
// -- Shortcodes cache
shortcodes = {}
// -- BCrypt
saltRounds = 10
// -- HashIDs
hashids = new Hashids("", 6)
// -- Ratelimit
ratelimit = {}
// -- Ip Bans
ignorelist = {}
+63
View File
@@ -0,0 +1,63 @@
# Status codes for API Endpoints
## General
|Code|Meaning|
|---|---|
|1|Hit ratelimit|
|2|Hit ratelimit too often.<br>You got banned|
|||
## Info
|Code|Meaning|
|---|---|
|10|success|
|11|shortcode non-existant|
|||
## createUser
|Code|Meaning|
|---|---|
|20|success|
|21|user already exists|
|22|error while creating user|
|23|Username length must be<br> between 3 and 32 characters|
|24|Password and username must not be empty or just spaces|
|||
## authenticate
|Code|Meaning|
|---|---|
|30|success|
|31|incorrect credentials|
|32|user does not exist|
|33|error while creating api key|
|||
## createCode
|Code|Meaning|
|---|---|
|40|success|
|41|no api key present|
|42|invalid api key|
|43|error while creating shortcode|
|44|you already have a shortcode for this url|
|45|unknown error|
|||
## codeList
|Code|Meaning|
|---|---|
|50|success|
|51|no api key present|
|52|invalid api key|
|||
## deleteCode
|Code|Meaning|
|---|---|
|60|success|
|61|no api key present|
|62|invalid api key|
|63|error while deleting shortcode|
|64|Not your shortcode|
|65|unknown error|
|||
+64
View File
@@ -0,0 +1,64 @@
-- Dumping database structure for url_shortener
CREATE DATABASE IF NOT EXISTS `url_shortener` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `url_shortener`;
-- Dumping structure for table url_shortener.tbl_access
CREATE TABLE IF NOT EXISTS `tbl_access` (
`idAccess` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fiShortcode` varchar(18) NOT NULL DEFAULT '0',
`dtIpHash` varchar(32) NOT NULL DEFAULT '0',
`dtTimestamp` varchar(20) NOT NULL DEFAULT '0',
PRIMARY KEY (`idAccess`),
KEY `FK_tbl_access_tbl_urls` (`fiShortcode`),
CONSTRAINT `FK_tbl_access_tbl_urls` FOREIGN KEY (`fiShortcode`) REFERENCES `tbl_urls` (`idShortcode`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=313 DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table url_shortener.tbl_bans
CREATE TABLE IF NOT EXISTS `tbl_bans` (
`idBan` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dtIpHash` varchar(32) NOT NULL,
`dtTimestamp` varchar(20) DEFAULT NULL,
PRIMARY KEY (`idBan`),
UNIQUE KEY `dtIpHash` (`dtIpHash`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table url_shortener.tbl_roles
CREATE TABLE IF NOT EXISTS `tbl_roles` (
`idRole` int(10) unsigned NOT NULL,
`dtPermissions` varchar(2048) DEFAULT NULL,
PRIMARY KEY (`idRole`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table url_shortener.tbl_urls
CREATE TABLE IF NOT EXISTS `tbl_urls` (
`idShortcode` varchar(18) NOT NULL,
`dtOriginal` varchar(2048) DEFAULT NULL,
`dtTimestamp` varchar(20) DEFAULT NULL,
`fiUser` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`idShortcode`),
KEY `FK_tbl_urls_tbl_users` (`fiUser`),
CONSTRAINT `FK_tbl_urls_tbl_users` FOREIGN KEY (`fiUser`) REFERENCES `tbl_users` (`idUser`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table url_shortener.tbl_users
CREATE TABLE IF NOT EXISTS `tbl_users` (
`idUser` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dtApiKey` varchar(100) DEFAULT NULL,
`dtUsername` varchar(32) NOT NULL DEFAULT '0',
`dtPassword` varchar(512) DEFAULT NULL,
`fiRole` int(11) unsigned DEFAULT NULL,
`dtTimestamp` varchar(20) DEFAULT NULL,
PRIMARY KEY (`idUser`),
UNIQUE KEY `dtUsername` (`dtUsername`),
KEY `FK_tbl_users_tbl_roles` (`fiRole`),
KEY `dtApiKey` (`dtApiKey`),
CONSTRAINT `FK_tbl_users_tbl_roles` FOREIGN KEY (`fiRole`) REFERENCES `tbl_roles` (`idRole`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/webui.iml" filepath="$PROJECT_DIR$/.idea/webui.iml" />
</modules>
</component>
</project>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.1" />
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>URL Shortener by Emily</title>
<script src="stuff/jquery-3.7.1.min.js"></script>
<script src="stuff/bootstrap.min.js"></script>
<link href="stuff/bootstrap.min.css" rel="stylesheet">
<link href="stuff/main.css" rel="stylesheet">
</head>
<body>
<div id="header">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Url Shortener</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="Navbar-content">
<li class="nav-item">
<a class="nav-link active fw-bold" href="javascript:;" id="nav-home">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="javascript:;" id="nav-shortcodes">Shortcodes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="javascript:;" id="nav-profile">Profile</a>
</li>
</ul>
<div id="profile-stuff">
<button class="btn btn-outline-primary me-2 hide">Register</button>
<button class="btn btn-outline-primary hide">Login</button>
</div>
</div>
</div>
</nav>
</div>
<div id="main-content">
</div>
<script>
$(document).ready(() => {
$("#main-content").load("pages/home.html")
// Navbar handler
// Yes, i am fully aware that my code is highly cringe, but its easy to understand, quick to write
// and most importantly, it gets the job done
// Feel free to suggest cleaner code tho if you know any
$("#nav-home").on("click", () => {
$("#nav-home").addClass("active").addClass("fw-bold")
$("#nav-profile").removeClass("active").removeClass("fw-bold")
$("#nav-shortcodes").removeClass("active").removeClass("fw-bold")
$("#main-content").load("pages/home.html")
})
$("#nav-shortcodes").on("click", () => {
$("#nav-shortcodes").addClass("active").addClass("fw-bold")
$("#nav-profile").removeClass("active").removeClass("fw-bold")
$("#nav-home").removeClass("active").removeClass("fw-bold")
$("#main-content").load("pages/shortcodes.html")
})
$("#nav-profile").on("click", () => {
$("#nav-profile").addClass("active").addClass("fw-bold")
$("#nav-shortcodes").removeClass("active").removeClass("fw-bold")
$("#nav-home").removeClass("active").removeClass("fw-bold")
$("#main-content").load("pages/profile.html")
})
})
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!--
Page content for home tab
-->
<div>
<p>Feel free to check out my <a href="https://git.dommymommy.xyz/emily/url-shortener">Git</a></p>
</div>
<script>
$("#test").html("lol")
</script>
+5
View File
@@ -0,0 +1,5 @@
<!--
Page content for profile tab
-->
<img src="stuff/IMG_1620.jpg">
+3
View File
@@ -0,0 +1,3 @@
<!--
Page content for shortcodes tab
-->
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

+6
View File
File diff suppressed because one or more lines are too long
+7
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
.hide{
display:none;
}