npm i enmap better-sqlite-pool koa koa-session koa-ejs koa-bodyparser koa-router bcrypt// Native Imports
const { sep, resolve, join } = require("path");
// Enmap Imports
const Enmap = require("enmap");
const users = new Enmap({ name: "users" });
// Bcrypt's hashing system
const bcrypt = require("bcrypt");
// Koa Imports
const Koa = require("koa");
// Koa's EJS renderer for HTML views.
const render = require("koa-ejs");
// The Body Parser to accept incoming form data and file uploads.
const parser = require("koa-bodyparser");
// The default "sessions" support.
const session = require("koa-session");
// Koa's get/post/etc router to simplify routes.
const Router = require("koa-router");
// Initializing the main components.
const router = new Router();
const app = new Koa();
// Define the data directory for templates (views).
const dataDir = resolve(`${process.cwd()}${sep}`);const newuser = (username, name, plainpw, admin = false) => {
if (users.has(username)) throw Error(`User ${username} already exists!`);
bcrypt.hash(plainpw, 10, (err, password) => {
if (err) throw err;
users.set(username, {
username, name, password, admin, created: Date.now()
});
});
};const login = (username, password) => {
const user = this.users.get(username);
if (!user) return new Promise(resp => resp(false));
if (!password) return new Promise(resp => resp(false));
return bcrypt.compare(password, user.password);
};app.keys = ['some secret hurr'];
app.use(session(app));render(app, {
root: join(__dirname, 'views'),
layout: 'template',
viewExt: 'html',
cache: false,
debug: true
});router.get("/", async (ctx, next) => {
// This is our static index page. It will render the views/index.js file
await ctx.render("index");
});// we obviously need a route to show the login page itself, too!
router.get("/login", async (ctx) => {
await ctx.render("login");
});
router.post("/login", async (ctx) => {
// Fail if there is no username and password.
// This relies on koa-bodyparser
if (!ctx.request.body.username || !ctx.request.body.password) {
ctx.throw(400, "Missing Username or Password");
}
// Use our login function to verify the username/password is correct
const success = await login(ctx.request.body.username, ctx.request.body.password);
if (success) {
// get the user's information
const user = users.get(ctx.request.body.username);
// Set all our session parameters:
ctx.session.logged = true;
ctx.session.username = ctx.request.body.username;
ctx.session.admin = user.admin;
ctx.session.name = user.name;
// Save the session itself. This sets the cookie in the browser,
// as well as save into the sessions in memory.
ctx.session.save();
console.log(`User authenticated: ${user.username}`);
// Once logged in, redirect to the secret page.
ctx.redirect("/secret");
} else {
console.log("Authentication Failed");
// Throw if the above login returns false.
ctx.throw(403, "Nope. Not allowed, mate.");
}
});router.get("/logout", async (ctx) => {
ctx.session = null;
ctx.redirect("/");
});router.get("/secret", async (ctx) => {
if(!ctx.session.logged) ctx.throw(403, "Unauthorized to view this page");
await ctx.render("secret");
});app
.use(parser())
.use(router.routes())
.use(router.allowedMethods());
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
app.listen(3000);<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>A Page</title>
</head>
<body>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/login">Login</a></li>
<li><a href="/secret">Secret</a></li>
</ul>
</nav>
<%- body %>
</body>
</html><h1>Index</h1>
<p>Lorem Ipsum Carrots</p><h1>Login</h1>
<form method="POST">
<p>Username: <input type="text" name="username" id="username"></p>
<p>Password: <input type="password" name="password" id="password"></p>
<p><button type="submit">Login</button></p>
</form><h1>Some Secret</h1>
<p>Han Shot First.</p>
// start discord.js init
// config with token and prefix.
const config = require("./config.json");
// Code below supports and is tested under "stable" 11.3.x
const Discord = require("discord.js");
const client = new Discord.Client();
// end discord.js init
// Initialize the server configurations
const Enmap = require('enmap');
// I attach settings to client to allow for modular bot setups
// In this example we'll leverage fetchAll:false and autoFetch:true for
// best efficiency in memory usage. We also have to use cloneLevel:'deep'
// to avoid our values to be "reference" to the default settings.
// The explanation for why is complex - just go with it.
client.settings = new Enmap({
name: "settings",
fetchAll: false,
autoFetch: true,
cloneLevel: 'deep'
});
// Just setting up a default configuration object here, to have somethign to insert.
const defaultSettings = {
prefix: "!",
modLogChannel: "mod-log",
modRole: "Moderator",
adminRole: "Administrator",
welcomeChannel: "welcome",
welcomeMessage: "Say hello to {{user}}, everyone!"
}const Enmap = require("enmap");
client.points = new Enmap({name: "points"});client.on("guildDelete", guild => {
// When the bot leaves or is kicked, delete settings to prevent stale entries.
client.settings.delete(guild.id);
});
client.on("guildMemberAdd", member => {
// This executes when a member joins, so let's welcome them!
// First, ensure the settings exist
client.settings.ensure(member.guild.id, defaultSettings);
// First, get the welcome message using get:
let welcomeMessage = client.settings.get(member.guild.id, "welcomeMessage");
// Our welcome message has a bit of a placeholder, let's fix that:
welcomeMessage = welcomeMessage.replace("{{user}}", member.user.tag)
// we'll send to the welcome channel.
member.guild.channels
.find("name", client.settings.get(member.guild.id, "welcomeChannel"))
.send(welcomeMessage)
.catch(console.error);
});client.on("message", async (message) => {
// This stops if it's not a guild (obviously), and we ignore all bots.
// Pretty standard for any bot.
if(!message.guild || message.author.bot) return;
// We can use ensure() to actually grab the default value for settings,
// if the key doesn't already exist.
const guildConf = client.settings.ensure(message.guild.id, defaultSettings);
// Now we can use the values!
// We stop processing if the message does not start with our prefix for this guild.
if(message.content.indexOf(guildConf.prefix) !== 0) return;
//Then we use the config prefix to get our arguments and command:
const args = message.content.split(/\s+/g);
const command = args.shift().slice(guildConf.prefix.length).toLowerCase();
// Commands Go Here
}); // Alright. Let's make a command! This one changes the value of any key
// in the configuration.
if(command === "setconf") {
// Command is admin only, let's grab the admin value:
const adminRole = message.guild.roles.find("name", guildConf.adminRole);
if(!adminRole) return message.reply("Administrator Role Not Found");
// Then we'll exit if the user is not admin
if(!message.member.roles.has(adminRole.id)) {
return message.reply("You're not an admin, sorry!");
}
// Let's get our key and value from the arguments.
// This is array destructuring, by the way.
const [prop, ...value] = args;
// Example:
// prop: "prefix"
// value: ["+"]
// (yes it's an array, we join it further down!)
// We can check that the key exists to avoid having multiple useless,
// unused keys in the config:
if(!client.settings.has(message.guild.id, prop)) {
return message.reply("This key is not in the configuration.");
}
// Now we can finally change the value. Here we only have strings for values
// so we won't bother trying to make sure it's the right type and such.
client.settings.set(message.guild.id, value.join(" "), prop);
// We can confirm everything's done to the client.
message.channel.send(`Guild configuration item ${prop} has been changed to:\n\`${value.join(" ")}\``);
} if(command === "showconf") {
let configProps = Object.keys(guildConf).map(prop => {
return `${prop} : ${guildConf[prop]}\n`;
});
message.channel.send(`The following are the server's current configuration:
\`\`\`${configProps}\`\`\``);
}client.on("message", message => {
if (message.author.bot) return;
// This is where we'll put our code.
if (message.content.indexOf(config.prefix) !== 0) return;
const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
const command = args.shift().toLowerCase();
// Command-specific code here!
});client.on("message", message => {
if (message.author.bot) return;
if (message.guild) {
// This is where we'll put our code.
}
// Rest of message handler
});client.on("message", message => {
if (message.author.bot) return;
if (message.guild) {
client.points.ensure(`${message.guild.id}-${message.author.id}`, {
user: message.author.id,
guild: message.guild.id,
points: 0,
level: 1
});
// Code continued...
}
// Rest of message handler
});client.on("message", message => {
if (message.author.bot) return;
if (message.guild) {
// Let's simplify the `key` part of this.
const key = `${message.guild.id}-${message.author.id}`;
client.points.ensure(key, {
user: message.author.id,
guild: message.guild.id,
points: 0,
level: 1
});
client.points.inc(key, "points");
}
// Rest of message handler
});const curLevel = Math.floor(0.1 * Math.sqrt(client.points.get(key, "points")));if (client.points.get(key, "level") < curLevel) {
message.reply(`You've leveled up to level **${curLevel}**! Ain't that dandy?`);
}client.points.set(key, curLevel, "level");client.on("message", message => {
// As usual, ignore all bots.
if (message.author.bot) return;
// If this is not in a DM, execute the points code.
if (message.guild) {
// We'll use the key often enough that simplifying it is worth the trouble.
const key = `${message.guild.id}-${message.author.id}`;
// Triggers on new users we haven't seen before.
client.points.ensure(`${message.guild.id}-${message.author.id}`, {
user: message.author.id,
guild: message.guild.id,
points: 0,
level: 1
});
client.points.inc(key, "points");
// Calculate the user's current level
const curLevel = Math.floor(0.1 * Math.sqrt(client.points.get(key, "points")));
// Act upon level up by sending a message and updating the user's level in enmap.
if (client.points.get(key, "level") < curLevel) {
message.reply(`You've leveled up to level **${curLevel}**! Ain't that dandy?`);
client.points.set(key, curLevel, "level");
}
}
// Rest of message handler
});client.on("message", message => {
if (message.author.bot) return;
if (message.guild) { /* Points Code Here */ }
if (message.content.indexOf(config.prefix) !== 0) return;
const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
const command = args.shift().toLowerCase();
// Command-specific code here!
}); if (command === "points") {
const key = `${message.guild.id}-${message.author.id}`;
return message.channel.send(`You currently have ${client.points.get(key, "points")} points, and are level ${client.points.get(key, "level")}!`);
}if(command === "leaderboard") {
// Get a filtered list (for this guild only), and convert to an array while we're at it.
const filtered = client.points.filter( p => p.guild === message.guild.id ).array();
// Sort it to get the top results... well... at the top. Y'know.
const sorted = filtered.sort((a, b) => b.points - a.points);
// Slice it, dice it, get the top 10 of it!
const top10 = sorted.splice(0, 10);
// Now shake it and show it! (as a nice embed, too!)
const embed = new Discord.RichEmbed()
.setTitle("Leaderboard")
.setAuthor(client.user.username, client.user.avatarURL)
.setDescription("Our top 10 points leaders!")
.setColor(0x00AE86);
for(const data of top10) {
embed.addField(client.users.get(data.user).tag, `${data.points} points (level ${data.level})`);
}
return message.channel.send({embed});
} if(command === "give") {
// Limited to guild owner - adjust to your own preference!
if(message.author.id !== message.guild.ownerID)
return message.reply("You're not the boss of me, you can't do that!");
const user = message.mentions.users.first() || client.users.get(args[0]);
if(!user) return message.reply("You must mention someone or give their ID!");
const pointsToAdd = parseInt(args[1], 10);
if(!pointsToAdd)
return message.reply("You didn't tell me how many points to give...")
// Ensure there is a points entry for this user.
client.points.ensure(`${message.guild.id}-${user.id}`, {
user: message.author.id,
guild: message.guild.id,
points: 0,
level: 1
});
// Get their current points.
let userPoints = client.points.get(`${message.guild.id}-${user.id}`, "points");
userPoints += pointsToAdd;
// And we save it!
client.points.set(`${message.guild.id}-${user.id}`, userPoints, "points")
message.channel.send(`${user.tag} has received ${pointsToAdd} points and now stands at ${userPoints} points.`);
}
if(command === "cleanup") {
// Let's clean up the database of all "old" users,
// and those who haven't been around for... say a month.
// Get a filtered list (for this guild only).
const filtered = client.points.filter( p => p.guild === message.guild.id );
// We then filter it again (ok we could just do this one, but for clarity's sake...)
// So we get only users that haven't been online for a month, or are no longer in the guild.
const rightNow = new Date();
const toRemove = filtered.filter(data => {
return !message.guild.members.has(data.user) || rightNow - 2592000000 > data.lastSeen;
});
toRemove.forEach(data => {
client.points.delete(`${message.guild.id}-${data.user}`);
});
message.channel.send(`I've cleaned up ${toRemove.size} old farts.`);
}