WIP Big refactor, using Mongoose now, refactor Plugins & Commands.
parent
4dfe0b836e
commit
ba5b154fad
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"watch": [ "src" ],
|
||||
"ext": ".ts,.js,.tsx,.json",
|
||||
"ignore": [],
|
||||
"exec": "ts-node --project ./tsconfig.json ./src/Main.ts -- --verbose"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
|
@ -4,24 +4,33 @@
|
|||
"description": "A simple discord bot for tracking user levels.",
|
||||
"main": "build/Main.js",
|
||||
"scripts": {
|
||||
"start": "node ./build/Main.js",
|
||||
"dev": "nodemon .",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Nicole Collings (Aurailus)",
|
||||
"author": "Auri Collings",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@types/node": "^12.12.22",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"bufferutil": "^4.0.1",
|
||||
"discord.js": "^11.5.1",
|
||||
"bufferutil": "^4.0.3",
|
||||
"discord.js": "^12.5.1",
|
||||
"file-api": "^0.10.4",
|
||||
"filereader": "^0.10.3",
|
||||
"form-data": "^2.5.1",
|
||||
"image-to-base64": "^2.0.1",
|
||||
"jimp": "^0.6.8",
|
||||
"form-data": "^4.0.0",
|
||||
"image-to-base64": "^2.1.1",
|
||||
"jimp": "^0.16.1",
|
||||
"lowdb": "^1.0.0",
|
||||
"request": "^2.88.0",
|
||||
"tsc": "^1.20150623.0",
|
||||
"mongodb": "^3.6.4",
|
||||
"mongoose": "^5.12.0",
|
||||
"request": "^2.88.2",
|
||||
"toml": "^3.0.0",
|
||||
"tslib": "^2.1.0",
|
||||
"xmlhttprequest": "^1.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mongodb": "^3.6.7",
|
||||
"@types/node": "^12.20.1",
|
||||
"log4js": "^6.3.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
|
227
src/Bot.ts
227
src/Bot.ts
|
@ -1,95 +1,166 @@
|
|||
const c = require('ansi-colors');
|
||||
const low = require('lowdb');
|
||||
const FileSync = require('lowdb/adapters/FileSync');
|
||||
// const low = require('lowdb');
|
||||
// const FileSync = require('lowdb/adapters/FileSync');
|
||||
|
||||
// import { MongoClient, Db } from 'mongodb';
|
||||
import Mongoose from 'mongoose';
|
||||
import * as Discord from 'discord.js';
|
||||
|
||||
import {getFatalCallback} from './Main';
|
||||
import {BotConf} from "./BotConf";
|
||||
import {BotStorage} from "./BotStorage";
|
||||
import {Database} from "./Database";
|
||||
// import {BotConf} from './BotConf';
|
||||
// import {BotStorage} from './BotStorage';
|
||||
// import {Database} from './Database';
|
||||
|
||||
import {Command} from "./Commands/Command";
|
||||
import {ChatChannels} from "./Modules/ChatChannels";
|
||||
import {Leveller} from "./Modules/Leveller";
|
||||
import LevelPlugin from './Plugin/Level/LevelPlugin';
|
||||
import VoiceChatPlugin from './Plugin/VoiceChat/VoiceChatPlugin';
|
||||
|
||||
import {Help} from "./Commands/Help";
|
||||
import {Level} from "./Commands/Level";
|
||||
import {Haystack} from "./Commands/Haystack";
|
||||
import {Leaderboard} from "./Commands/Leaderboard";
|
||||
// import {ChatChannels} from './Modules/ChatChannels';
|
||||
|
||||
export class Bot {
|
||||
config: BotConf;
|
||||
client: Discord.Client;
|
||||
storage: BotStorage;
|
||||
import { Command, CommandFn } from './Commands/Command';
|
||||
import Help from './Commands/Help';
|
||||
// import {Level} from './Commands/Level';
|
||||
// import {Haystack} from './Commands/Haystack';
|
||||
// import {Leaderboard} from './Commands/Leaderboard';
|
||||
|
||||
chatChannels: ChatChannels;
|
||||
leveller: Leveller;
|
||||
import log4js from 'log4js';
|
||||
|
||||
commands: Command[] = [];
|
||||
const logger = log4js.getLogger();
|
||||
|
||||
constructor(config: BotConf) {
|
||||
this.config = config;
|
||||
this.client = new Discord.Client();
|
||||
this.storage = new BotStorage(config);
|
||||
export interface BotConfig {
|
||||
auth: {
|
||||
discord: string;
|
||||
mongo_url: string;
|
||||
mongo_db: string;
|
||||
};
|
||||
|
||||
const adapter = new FileSync('./data/db.json');
|
||||
this.storage.db = new Database(low(adapter));
|
||||
options: {
|
||||
status: string
|
||||
prefix: string;
|
||||
delete_triggers: boolean;
|
||||
}
|
||||
|
||||
connect(): Promise<Bot> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.login(this.config.token);
|
||||
|
||||
this.client.on('ready', () => {
|
||||
console.log(`Successfully connected as ${c.cyan(this.client.user.tag)}.`);
|
||||
console.log(`Version 1.0.1`);
|
||||
this.client.user.setActivity(this.config.playing_tag.message, {type: this.config.playing_tag.type});
|
||||
this.client.user.setStatus('online');
|
||||
resolve(this);
|
||||
});
|
||||
|
||||
this.client.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.client.on('message', (msg) => {
|
||||
for (let command of this.commands) {
|
||||
if (msg.content.substr(0, command.prefix.length).toLowerCase() == command.prefix.toLowerCase()) {
|
||||
command.exec(msg);
|
||||
return;
|
||||
}
|
||||
plugin: {
|
||||
level?: {
|
||||
please_and_thank_you: boolean;
|
||||
message: {
|
||||
cooldown: number;
|
||||
min_length: number;
|
||||
},
|
||||
levels: {
|
||||
[num: string]: {
|
||||
name: string;
|
||||
experience: number;
|
||||
role: number;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
bindFunctions(): Promise<Bot> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.chatChannels = new ChatChannels(this.client, this.storage);
|
||||
this.leveller = new Leveller(this.client, this.storage);
|
||||
|
||||
this.commands.push(new Help(this.client, this.storage));
|
||||
this.commands.push(new Level(this.client, this.storage));
|
||||
this.commands.push(new Haystack(this.client, this.storage));
|
||||
this.commands.push(new Leaderboard(this.client, this.storage));
|
||||
|
||||
resolve(this);
|
||||
}
|
||||
catch (e) { reject(e); }
|
||||
});
|
||||
}
|
||||
|
||||
async shutDown() {
|
||||
try {
|
||||
await this.chatChannels.cleanup();
|
||||
await this.leveller.cleanup();
|
||||
|
||||
console.log(`Shut down gracefully.`);
|
||||
}
|
||||
catch (e) {
|
||||
getFatalCallback("Shutdown")(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class Bot {
|
||||
private config: BotConfig;
|
||||
private client: Discord.Client;
|
||||
// private db: ;
|
||||
// storage: BotStorage;
|
||||
|
||||
// chatChannels: ChatChannels;
|
||||
// leveller: Leveller;
|
||||
|
||||
private plugins: any[] = [];
|
||||
private commands: { [command: string]: Command | CommandFn } = {};
|
||||
|
||||
constructor(config: BotConfig) {
|
||||
this.config = config;
|
||||
this.client = new Discord.Client();
|
||||
// this.storage = new BotStorage(config);
|
||||
|
||||
// const adapter = new FileSync('./data/db.json');
|
||||
// this.storage.db = new Database(low(adapter));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the connection to discord, and binds a shutdown handler.
|
||||
*
|
||||
* @returns the bot, once initialization is complete.
|
||||
*/
|
||||
|
||||
async init(): Promise<this> {
|
||||
await this.connect();
|
||||
await this.bind();
|
||||
|
||||
process.on('SIGINT', () => this.onInterrupt().then(() => process.exit()));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to connect the client to discord, and sets its status to online.
|
||||
*
|
||||
* @returns a promise indicating the success state of the connection.
|
||||
*/
|
||||
|
||||
private async connect() {
|
||||
await new Promise((resolve, reject) => {
|
||||
Mongoose.connect(this.config.auth.mongo_url, { useNewUrlParser: true, useUnifiedTopology: true });
|
||||
Mongoose.set('useFindAndModify', false);
|
||||
Mongoose.connection.on('error', reject);
|
||||
Mongoose.connection.once('open', resolve);
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this.client.login(this.config.auth.discord);
|
||||
this.client.on('error', reject);
|
||||
this.client.once('ready', () => {
|
||||
const user = this.client.user as Discord.ClientUser;
|
||||
user.setPresence({
|
||||
status: 'online',
|
||||
activity: { name: this.config.options.status, type: 'CUSTOM_STATUS' }
|
||||
});
|
||||
logger.info('Successfully connected as %s.', user.tag);
|
||||
resolve(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Binds plugins to the bot.
|
||||
*/
|
||||
|
||||
private bind() {
|
||||
this.plugins.push(new LevelPlugin(this.config as any, this.client, this.commands));
|
||||
this.plugins.push(new VoiceChatPlugin(this.config as any, this.client));
|
||||
|
||||
this.commands.help = Help;
|
||||
|
||||
// this.chatChannels = new ChatChannels(this.client, this.storage);
|
||||
// this.leveller = new Leveller(this.client, this.storage);
|
||||
// this.commands.push(new Haystack(this.client, this.storage));
|
||||
// this.commands.push(new Leaderboard(this.client, this.storage));
|
||||
|
||||
this.client.on('message', (msg) => {
|
||||
if (!msg.content.startsWith(this.config.options.prefix + ' ')) return;
|
||||
const full = msg.content.substr(this.config.options.prefix.length + 1).trim();
|
||||
const command = full.substr(0, full.indexOf(' ') === -1 ? full.length : full.indexOf(' ')).toLowerCase().trimLeft();
|
||||
const args = command.substr(command.length).trimLeft().split(' ');
|
||||
const cmd = this.commands[command];
|
||||
if (typeof cmd === 'function') cmd(msg, command, args);
|
||||
else if (typeof cmd === 'object') cmd.trigger(msg, command, args);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs cleanup activities. Bound to SIGINT after the bot has been set up.
|
||||
*/
|
||||
|
||||
private async onInterrupt() {
|
||||
try {
|
||||
await Promise.all(this.plugins.map(async p => p.cleanup?.()));
|
||||
logger.info('Shut down successfully.');
|
||||
}
|
||||
catch (e) {
|
||||
logger.fatal('Error shutting down k9:\n%s', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
export class BotConf {
|
||||
token: string;
|
||||
playing_tag: {
|
||||
type: Discord.ActivityType,
|
||||
message: string
|
||||
};
|
||||
command_prefix: string;
|
||||
delete_triggers: boolean;
|
||||
xp_properties: {
|
||||
level_base_cost: number,
|
||||
level_multiplier: number
|
||||
};
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import {BotConf} from "./BotConf";
|
||||
import {GuildData} from "./GuildData";
|
||||
import {Database} from "./Database";
|
||||
|
||||
export class BotStorage {
|
||||
db: Database;
|
||||
conf: BotConf;
|
||||
guildData: {[key: string]: GuildData} = {};
|
||||
|
||||
constructor(conf: BotConf) {
|
||||
this.conf = conf;
|
||||
}
|
||||
|
||||
getGuild(guild: Discord.Guild): GuildData {
|
||||
if (!this.guildData[guild.id]) {
|
||||
this.guildData[guild.id] = new GuildData(guild);
|
||||
}
|
||||
return this.guildData[guild.id];
|
||||
}
|
||||
}
|
|
@ -1,48 +1,7 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import {BotStorage} from "../BotStorage";
|
||||
export type CommandFn = (msg: Discord.Message, command: string, args: string[]) => void;
|
||||
|
||||
export class Command {
|
||||
client: Discord.Client;
|
||||
storage: BotStorage;
|
||||
prefix: string;
|
||||
|
||||
constructor(client: Discord.Client, storage: BotStorage) {
|
||||
this.client = client;
|
||||
this.storage = storage;
|
||||
this.prefix = storage.conf.command_prefix + " ";
|
||||
}
|
||||
|
||||
exec(msg: Discord.Message) {/*Virtual Method*/}
|
||||
|
||||
deleteTrigger(msg: Discord.Message) {
|
||||
if (this.storage.conf.delete_triggers) {
|
||||
msg.delete().catch((e) => this.sendErrorMessage(msg, e));
|
||||
}
|
||||
}
|
||||
|
||||
sendErrorMessage(msg: Discord.Message, e: Discord.DiscordAPIError | string) {
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setAuthor("Error", "https://i.imgur.com/qSHm1lQ.png")
|
||||
.setColor("#D60058")
|
||||
.setFooter(`Requested by ${(msg.member) ? msg.member.displayName : msg.author.username}`, msg.author.avatarURL)
|
||||
.setTimestamp();
|
||||
|
||||
if (typeof e == "string") {
|
||||
embed.setDescription(e);
|
||||
}
|
||||
else {
|
||||
embed.setDescription(`An unknown error occured: ${e.message}`)
|
||||
switch (e.message) {
|
||||
case "Missing Permissions": {
|
||||
embed.setDescription(`\`delete_triggers\` is set to true, but the bot does not have the \`Manage Messages\` permission.`);
|
||||
}
|
||||
case "Cannot execute action on a DM channel": {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg.channel.send({embed}).catch((e) => {/*Missing send message permissions for the channel*/});
|
||||
}
|
||||
export interface Command {
|
||||
trigger: CommandFn;
|
||||
}
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
const requestPost = require('request');
|
||||
const requestGet = require('request').defaults({encoding: null});
|
||||
|
||||
import * as Discord from 'discord.js';
|
||||
|
||||
import {Command} from "./Command"
|
||||
import {BotStorage} from "../BotStorage";
|
||||
|
||||
export class Haystack extends Command {
|
||||
constructor(client: Discord.Client, storage: BotStorage) {
|
||||
super(client, storage);
|
||||
this.prefix += "haystack";
|
||||
}
|
||||
|
||||
exec(msg: Discord.Message) {
|
||||
|
||||
if (msg.attachments.size == 0) {
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setAuthor("Haystack", "https://i.imgur.com/qSHm1lQ.png")
|
||||
.setColor("#D60058")
|
||||
.setDescription(`Please Attach an image to use Haystack.`)
|
||||
.setFooter(`Requested by ${(msg.member) ? msg.member.displayName : msg.author.username}`, msg.author.avatarURL)
|
||||
.setTimestamp();
|
||||
|
||||
msg.channel.send(embed).catch((e) => {/*Missing permissions to send to channel*/});
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setAuthor("Haystack", "https://i.imgur.com/pPObkMW.png")
|
||||
.setColor("#7189D8")
|
||||
.setDescription(`Preparing...`)
|
||||
.setFooter(`Requested by ${(msg.member) ? msg.member.displayName : msg.author.username}`, msg.author.avatarURL)
|
||||
.setTimestamp();
|
||||
|
||||
msg.channel.send(embed).then(newMsg => {
|
||||
const errEmbed = new Discord.RichEmbed()
|
||||
.setAuthor("Haystack", "https://i.imgur.com/qSHm1lQ.png")
|
||||
.setColor("#D60058")
|
||||
.setFooter(`Requested by ${(msg.member) ? msg.member.displayName : msg.author.username}`, msg.author.avatarURL)
|
||||
.setTimestamp();
|
||||
|
||||
if (Array.isArray(newMsg)) {
|
||||
errEmbed.setDescription(`Internal error: [Array.isArray]`);
|
||||
|
||||
(newMsg as any as Discord.Message).edit(errEmbed);
|
||||
return;
|
||||
}
|
||||
|
||||
const imageUrl = msg.attachments.first().url;
|
||||
|
||||
requestGet.get(imageUrl, (err, response, body) => {
|
||||
if (err) {
|
||||
errEmbed.setDescription(`There was an error getting the image: ${err}`);
|
||||
|
||||
(newMsg as any as Discord.Message).edit(errEmbed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
const newEmbed = new Discord.RichEmbed()
|
||||
.setAuthor("Haystack", "https://i.imgur.com/pPObkMW.png")
|
||||
.setColor("#7189D8")
|
||||
.setDescription(`Processing Image...`)
|
||||
.setFooter(`Requested by ${(msg.member) ? msg.member.displayName : msg.author.username}`, msg.author.avatarURL)
|
||||
.setTimestamp();
|
||||
|
||||
(newMsg as any as Discord.Message).edit(newEmbed);
|
||||
|
||||
requestPost.post({
|
||||
url: "https://api.haystack.ai/api/image/analyze?output=json&apikey=c91b373cc011946774767cf7220d7f64&model=age&model=gender&model=attractiveness",
|
||||
body: new Buffer(body)
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
errEmbed.setDescription(`There was an error processing the image: ${err}`);
|
||||
(newMsg as any as Discord.Message).edit(errEmbed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
let json: any = JSON.parse(body);
|
||||
|
||||
if (json.people.length == 0) {
|
||||
errEmbed.setDescription(`Haystack can't find any people in the image.`);
|
||||
(newMsg as any as Discord.Message).edit(errEmbed);
|
||||
return;
|
||||
}
|
||||
if (json.people.length > 1) {
|
||||
errEmbed.setDescription(`Haystack found multiple people in the image.\nThe \`k9 haystack\` command only supports one.`);
|
||||
(newMsg as any as Discord.Message).edit(errEmbed);
|
||||
return;
|
||||
}
|
||||
|
||||
let person: any = json.people[0];
|
||||
let gender = (person.gender.gender == "female") ? "Female" : "Male";
|
||||
|
||||
newEmbed.setDescription(`Powered by [Haystack.ai](https://haystack.ai)`);
|
||||
newEmbed.addField("Age", person.age);
|
||||
newEmbed.addField("Gender", `${gender} (${Math.round(person.gender.confidence * 100)}%)`);
|
||||
newEmbed.addField("Attractiveness", `${Math.round(person.attractiveness * 100) / 100} / 10`);
|
||||
newEmbed.setImage(imageUrl);
|
||||
}
|
||||
else {
|
||||
errEmbed.setDescription(`[${response.statusCode}] ${body}`);
|
||||
(newMsg as any as Discord.Message).edit(errEmbed);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,31 +1,19 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import {Command} from "./Command"
|
||||
import {BotStorage} from "../BotStorage";
|
||||
export default function Help(msg: Discord.Message) {
|
||||
msg.channel.send({
|
||||
embed: new Discord.MessageEmbed()
|
||||
.setAuthor('K9 Help', 'https://cdn.discordapp.com/avatars/613569990297255938/5c6883f8b8f324fe38cf5d1a8361339a.webp?size=64')
|
||||
.setColor('#EE86ED')
|
||||
.setDescription(
|
||||
'Hi, I\'m k9! I\'m a user level tracking bot made by Auri#1311. ' +
|
||||
'I assign users levels and automagically grants users roles once they reach certain level thresholds. ' +
|
||||
'I also have a few simple commands available to interact with me.')
|
||||
.setFooter(`Requested by ${(msg.member) ? msg.member.displayName : msg.author.username}`, msg.author.avatarURL({ size: 32 })!)
|
||||
.setTimestamp()
|
||||
|
||||
export class Help extends Command {
|
||||
constructor(client: Discord.Client, storage: BotStorage) {
|
||||
super(client, storage);
|
||||
this.prefix += "help";
|
||||
}
|
||||
|
||||
exec(msg: Discord.Message) {
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setAuthor("K9 Help", "https://cdn.discordapp.com/avatars/613569990297255938/13a0f7a3818feaa9cbc173f54b30eb9c.png?size=128")
|
||||
.setColor("#EE86ED")
|
||||
.setDescription(
|
||||
`Hi, I'm k9! I'm a user level tracking bot made by Aurailus#4014. ` +
|
||||
`I assign users levels and automagically grants users roles once they reach certain level thresholds. ` +
|
||||
`I also have a few simple commands available to interact with me.`)
|
||||
.setFooter(`Requested by ${(msg.member) ? msg.member.displayName : msg.author.username}`, msg.author.avatarURL)
|
||||
.setTimestamp()
|
||||
|
||||
.addField("⠀`k9 help`", `⠀Sends this message.`)
|
||||
.addField("⠀`k9 level`", `⠀Displays your level, XP, and rank.`)
|
||||
.addField("⠀`k9 leaderboard`", `⠀Shows the top ranked users in the current server.`)
|
||||
.addField("⠀`k9 haystack`", `⠀Runs an attached image through haystack.ai to determine various statistics.`)
|
||||
|
||||
msg.channel.send({embed}).catch((e) => {/*Missing send message permissions for the channel*/});
|
||||
super.deleteTrigger(msg);
|
||||
}
|
||||
.addField('`k9 help`', '⠀Sends this message.')
|
||||
.addField('`k9 level`', '⠀Displays your level, XP, and rank.')
|
||||
.addField('`k9 leaderboard`', '⠀Shows the top ranked users in the current server.')
|
||||
}).catch(_ => { /* Missing send permissions. */ });
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import {Command} from "./Command"
|
||||
import {BotStorage} from "../BotStorage";
|
||||
import {BotLevelRoles, Database, DBServer, DBUser} from "../Database";
|
||||
|
||||
export class Leaderboard extends Command {
|
||||
constructor(client: Discord.Client, storage: BotStorage) {
|
||||
super(client, storage);
|
||||
this.prefix += "leaderboard";
|
||||
}
|
||||
|
||||
exec(msg: Discord.Message) {
|
||||
if (!msg.guild) {
|
||||
super.sendErrorMessage(msg, "This command must be called from within a server.");
|
||||
return;
|
||||
}
|
||||
|
||||
let server: DBServer = this.storage.db.getServer(msg.guild);
|
||||
// let user: DBUser = server.getUser(msg.member);
|
||||
|
||||
let users: DBUser[] = this.storage.db.getServer(msg.guild).getTopUsers();
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setAuthor("Leaderboard", "https://i.imgur.com/LaPvO6n.png")
|
||||
.setColor("#FFAC38")
|
||||
.setDescription(`The most active members in ${msg.guild.name}.`)
|
||||
.setFooter(`Requested by ${msg.member.displayName}`, msg.author.avatarURL)
|
||||
.setTimestamp()
|
||||
|
||||
.addBlankField();
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = msg.guild.members.get(users[i].id);
|
||||
let name = "Removed";
|
||||
if (user) name = user.displayName;
|
||||
if (name.length >= 20) name = name.substr(0, 18) + "...";
|
||||
embed.addField(
|
||||
`⠀${i < 3 ? "**" : ""}${i + 1}) ${name}${i < 3 ? "**" : ""}`,
|
||||
`⠀Level ${users[i].level} • ${Math.floor(users[i].totalXP)} XP`, true);
|
||||
}
|
||||
|
||||
embed.addBlankField();
|
||||
|
||||
msg.channel.send({embed}).catch(e => {/*Missing send message permissions for the channel*/});
|
||||
super.deleteTrigger(msg);
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import {Command} from "./Command"
|
||||
import {BotStorage} from "../BotStorage";
|
||||
import {BotLevelRoles, Database, DBServer, DBUser} from "../Database";
|
||||
|
||||
export class Level extends Command {
|
||||
constructor(client: Discord.Client, storage: BotStorage) {
|
||||
super(client, storage);
|
||||
this.prefix += "level";
|
||||
}
|
||||
|
||||
exec(msg: Discord.Message) {
|
||||
if (!msg.guild) {
|
||||
super.sendErrorMessage(msg, "This command must be called from within a server.");
|
||||
return;
|
||||
}
|
||||
|
||||
let server: DBServer = this.storage.db.getServer(msg.guild);
|
||||
let user: DBUser = server.getUser(msg.member);
|
||||
|
||||
const cost = (this.storage.conf.xp_properties.level_base_cost + Math.pow(user.level, this.storage.conf.xp_properties.level_multiplier));
|
||||
|
||||
let currentRole = -1;
|
||||
|
||||
const roles = server.getLevelRolesTable();
|
||||
for (let role in roles) {
|
||||
let num = parseInt(role);
|
||||
if (num <= user.level && num > currentRole) currentRole = num;
|
||||
}
|
||||
|
||||
let role = (currentRole == -1) ? "Potato" : msg.guild.roles.find(r => r.id == roles[currentRole]).name;
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setAuthor("My Level", "https://i.imgur.com/Nqyb94h.png")
|
||||
.setColor("#15B5A6")
|
||||
.setDescription(`Statistics for ${msg.member.displayName} in ${msg.guild.name}.`)
|
||||
.setFooter(`Requested by ${msg.member.displayName}`, msg.author.avatarURL)
|
||||
.setTimestamp()
|
||||
|
||||
.addField(`⠀Level`, `⠀${user.level}`, true)
|
||||
.addField(`⠀XP`, `⠀${Math.floor(user.levelXP)} / ${Math.ceil(cost)}`, true)
|
||||
.addField(`⠀Rank`, `⠀${role}`, true)
|
||||
|
||||
msg.channel.send({embed}).catch(e => {/*Missing send message permissions for the channel*/});
|
||||
super.deleteTrigger(msg);
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
export class Database {
|
||||
db: any; //LowDB
|
||||
|
||||
constructor(db: any) {
|
||||
this.db = db;
|
||||
this.db.defaults({ servers: [] }).write();
|
||||
}
|
||||
|
||||
getServer(guild: Discord.Guild): DBServer {
|
||||
let server: any = this.db.get('servers').find({id: guild.id});
|
||||
if (!server.value()) {
|
||||
const serverTable = {
|
||||
id: guild.id,
|
||||
levelRoles: {},
|
||||
users: []
|
||||
}
|
||||
this.db.get('servers').push(serverTable).write();
|
||||
return new DBServer(this.db, this.db.get('servers').find({id: guild.id}));
|
||||
}
|
||||
return new DBServer(this.db, server);
|
||||
}
|
||||
}
|
||||
|
||||
export class DBServer {
|
||||
id: string;
|
||||
server: any;
|
||||
db: any;
|
||||
|
||||
constructor(db: any, server: any /*LowDB result*/) {
|
||||
this.id = server.value().id;
|
||||
this.db = db;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
getLevelRolesTable() : BotLevelRoles {
|
||||
return this.server.get('levelRoles').value();
|
||||
}
|
||||
|
||||
getUser(member: Discord.GuildMember) : DBUser {
|
||||
let user: any = this.server.get('users').find({id: member.id});
|
||||
if (!user.value()) {
|
||||
const userTable: DBUser = {
|
||||
id: member.id,
|
||||
lastInstigated: Date.now() - 60 * 1000,
|
||||
lastPosted: Date.now() - 60 * 1000,
|
||||
|
||||
level: 0,
|
||||
levelXP: 0,
|
||||
totalXP: 0,
|
||||
|
||||
messages: 0
|
||||
}
|
||||
this.server.get('users').push(userTable).write();
|
||||
return this.server.get('users').find({id: member.id});
|
||||
}
|
||||
return user.value();
|
||||
}
|
||||
|
||||
getTopUsers() : DBUser[] {
|
||||
return this.server.get('users').sortBy('totalXP').reverse().take(9).value();
|
||||
}
|
||||
|
||||
pushUser(user: DBUser) {
|
||||
this.server.get('users').find({id: user.id}).assign(user).write();
|
||||
}
|
||||
}
|
||||
|
||||
export interface BotLevelRoles {[key: string]: string}
|
||||
|
||||
export interface DBUser {
|
||||
id: string,
|
||||
lastInstigated: number,
|
||||
lastPosted: number,
|
||||
|
||||
level: number,
|
||||
levelXP: number,
|
||||
totalXP: number,
|
||||
|
||||
messages: number
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
export class GuildData {
|
||||
guild: Discord.Guild;
|
||||
chatChannels: {[key: string]: Discord.Snowflake} = {};
|
||||
|
||||
constructor(guild: Discord.Guild) {
|
||||
this.guild = guild;
|
||||
}
|
||||
}
|
48
src/Main.ts
48
src/Main.ts
|
@ -1,38 +1,18 @@
|
|||
const fs = require('fs').promises;
|
||||
const c = require('ansi-colors');
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import * as Discord from 'discord.js';
|
||||
import log4js from 'log4js';
|
||||
import { parse } from 'toml';
|
||||
import Bot, { BotConfig } from "./Bot";
|
||||
|
||||
import {BotConf} from "./BotConf";
|
||||
import {Bot} from "./Bot";
|
||||
const logger = log4js.getLogger();
|
||||
logger.level = 'debug';
|
||||
|
||||
export function getFatalCallback(prefix: string, exit: boolean = true) {
|
||||
return function(err: Error) {
|
||||
console.error(c.bgRed.bold.white(`[${prefix}] A fatal error has occured:\n${err.toString()}.\n`));
|
||||
if (exit) process.exit(0);
|
||||
(async () => {
|
||||
try {
|
||||
const conf = parse((await fs.readFile('./data/conf.toml')).toString()) as BotConfig;
|
||||
await new Bot(conf).init();
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
fs.access("./data").then(() => {
|
||||
return fs.access("./data/conf.json");
|
||||
}).then(() => {
|
||||
return fs.readFile("./data/conf.json");
|
||||
}).then((resp: Buffer) => {
|
||||
try {
|
||||
const conf: BotConf = JSON.parse(resp.toString());
|
||||
let bot = new Bot(conf);
|
||||
return bot.connect();
|
||||
}
|
||||
catch(e) { getFatalCallback("Conf Parsing")(e); }
|
||||
}).then((bot: Bot) => {
|
||||
return bot.bindFunctions();
|
||||
}).then((bot: Bot) => {
|
||||
process.on('SIGINT', async () => {
|
||||
await bot.shutDown();
|
||||
process.exit();
|
||||
});
|
||||
}).catch(getFatalCallback("Main.ts"));
|
||||
}
|
||||
|
||||
start();
|
||||
catch (e) {
|
||||
logger.fatal('Error initializing k9:\n%s', e);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import {BotStorage} from "../BotStorage";
|
||||
import {GuildData} from "../GuildData";
|
||||
|
||||
export class ChatChannels {
|
||||
client: Discord.Client;
|
||||
storage: BotStorage;
|
||||
|
||||
PREFIX = "**Temporary discussion for ";
|
||||
SUFFIX = ".**"
|
||||
|
||||
constructor(client: Discord.Client, storage: BotStorage) {
|
||||
this.client = client;
|
||||
this.storage = storage;
|
||||
|
||||
client.on("voiceStateUpdate", (om, nm) => this.voiceStateUpdate(om, nm));
|
||||
|
||||
this.client.guilds.forEach((guild, guildKey) => {
|
||||
let guildData = this.storage.getGuild(guild);
|
||||
guild.channels.forEach((channel, channelKey) => {
|
||||
if (channel.type == "voice") {
|
||||
if ((channel as Discord.VoiceChannel).members.size >= 1 && (channel as Discord.VoiceChannel).name != "afk") {
|
||||
this.createChatChannel(channel as Discord.VoiceChannel, guildData);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
voiceStateUpdate(oldMember: Discord.GuildMember, member: Discord.GuildMember): void {
|
||||
let guild: GuildData = this.storage.getGuild(member.guild);
|
||||
|
||||
if (member.voiceChannel == oldMember.voiceChannel) return;
|
||||
|
||||
if (member.voiceChannel && member.voiceChannel.parent) {
|
||||
if (member.voiceChannel.members.size == 1) {
|
||||
this.createChatChannel(member.voiceChannel, guild);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldMember.voiceChannel && oldMember.voiceChannel.parent) {
|
||||
if (oldMember.voiceChannel.members.size == 0) {
|
||||
let channelId = guild.chatChannels[oldMember.voiceChannelID];
|
||||
if (oldMember.client.channels.get(channelId)) oldMember.client.channels.get(channelId).delete()
|
||||
.catch((e) => {/*Channel was already deleted*/})
|
||||
.finally(() => {
|
||||
delete guild.chatChannels[oldMember.voiceChannelID];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createChatChannel(voiceChannel: Discord.VoiceChannel, guild: GuildData) {
|
||||
let channelName = voiceChannel.name.replace(/[\W_]+/g,"-") + "-chat";
|
||||
|
||||
voiceChannel.guild.createChannel(channelName, {
|
||||
type: `text`,
|
||||
topic: `${this.PREFIX}<#${voiceChannel.id}>${this.SUFFIX}`,
|
||||
parent: voiceChannel.parent
|
||||
}).then((channel: Discord.TextChannel) => {
|
||||
guild.chatChannels[voiceChannel.id] = channel.id;
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setAuthor(channelName, "https://i.imgur.com/vitVUtr.png")
|
||||
.setColor("#EE86ED")
|
||||
.setDescription(
|
||||
`This is a temporary discussion channel for ${channelName}!\n` +
|
||||
`This channel will be automatically deleted when everybody leaves the voice channel.\n`)
|
||||
.setTimestamp()
|
||||
|
||||
channel.send({embed}).catch((e) => {/*Missing send message permissions for the channel*/});
|
||||
|
||||
}).catch((e) => {/*Channel was deleted before the promise callback*/});
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
for (let key in this.storage.guildData) {
|
||||
let guild: GuildData = this.storage.guildData[key];
|
||||
for (let key in guild.chatChannels) {
|
||||
let chat = guild.chatChannels[key];
|
||||
await guild.guild.channels.get(chat).delete().catch(e => {/*Channel was already deleted*/});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
const fs = require('fs');
|
||||
|
||||
import * as Discord from 'discord.js';
|
||||
|
||||
import {BotStorage} from "../BotStorage";
|
||||
import {GuildData} from "../GuildData";
|
||||
import {LevelImageBuilder} from "./LevelImageBuilder"
|
||||
import {BotLevelRoles, Database, DBServer, DBUser} from "../Database";
|
||||
|
||||
export class Leveller {
|
||||
client: Discord.Client;
|
||||
storage: BotStorage;
|
||||
imgBuilder: LevelImageBuilder;
|
||||
checkVAInterval: any;
|
||||
|
||||
constructor(client: Discord.Client, storage: BotStorage) {
|
||||
this.client = client;
|
||||
this.storage = storage;
|
||||
this.imgBuilder = new LevelImageBuilder();
|
||||
|
||||
client.on("message", (msg) => this.onMessage(msg));
|
||||
this.checkVAInterval = setInterval(this.checkVoiceActivity.bind(this), 5*1000*60);
|
||||
}
|
||||
|
||||
checkVoiceActivity(): void {
|
||||
for (let guildKey in this.storage.guildData) {
|
||||
let guild = this.storage.guildData[guildKey];
|
||||
for (let activeVoiceChannel in guild.chatChannels) {
|
||||
let channel = guild.guild.channels.get(activeVoiceChannel) as Discord.VoiceChannel;
|
||||
let undeafenedUsers: number = 0;
|
||||
|
||||
channel.members.forEach((member, key) => {
|
||||
if (!member.selfDeaf) undeafenedUsers++;
|
||||
});
|
||||
|
||||
if (undeafenedUsers >= 2) {
|
||||
channel.members.forEach((member, key) => {
|
||||
if (!member.selfDeaf && !member.selfMute) {
|
||||
|
||||
let chatChannel = (guild.guild.channels.get(guild.chatChannels[activeVoiceChannel]) as Discord.TextChannel);
|
||||
|
||||
let server: DBServer = this.storage.db.getServer(guild.guild);
|
||||
let user: DBUser = server.getUser(member);
|
||||
|
||||
let xp = Math.round(Math.random() + 0.3);
|
||||
|
||||
user.levelXP += xp;
|
||||
user.totalXP += xp;
|
||||
|
||||
const cost = (this.storage.conf.xp_properties.level_base_cost + Math.pow(user.level, this.storage.conf.xp_properties.level_multiplier));
|
||||
|
||||
if (user.levelXP >= cost) {
|
||||
user.level++;
|
||||
user.levelXP -= cost;
|
||||
|
||||
if (chatChannel) {
|
||||
this.imgBuilder.generate(member.displayName, user.level, member.id).then(image => {
|
||||
chatChannel.send("", {file: image as any}).then(() => {
|
||||
fs.unlinkSync(image);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let currentRole = -1;
|
||||
let previousRole = -1;
|
||||
|
||||
const roles: BotLevelRoles = server.getLevelRolesTable();
|
||||
|
||||
for (let role in roles) {
|
||||
let num = parseInt(role);
|
||||
if (num <= user.level && num > currentRole) currentRole = num;
|
||||
if (num <= user.level - 1 && num > previousRole) previousRole = num;
|
||||
}
|
||||
|
||||
if (currentRole != previousRole) {
|
||||
if (previousRole != -1) member.removeRole(member.guild.roles.find(r => r.id == roles[previousRole]));
|
||||
if (currentRole != -1) member.addRole(member.guild.roles.find(r => r.id == roles[currentRole]), 'Update user level role.');
|
||||
}
|
||||
}
|
||||
|
||||
server.pushUser(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(msg: Discord.Message): void {
|
||||
|
||||
// Don't allow the bot itself to gain levels.
|
||||
if (msg.author.id == this.client.user.id) return;
|
||||
|
||||
// Don't count bot commands.
|
||||
if (msg.content.substr(0, this.storage.conf.command_prefix.length).toLowerCase() == this.storage.conf.command_prefix) return;
|
||||
|
||||
// Enforce minimum content requirements for XP gain
|
||||
// Don't count messages less than N characters and without a space.
|
||||
if (msg.content.length < 6 || msg.content.split(" ").length - 1 < 1) return;
|
||||
|
||||
// Don't count DM conversations
|
||||
if (!msg.guild) return;
|
||||
|
||||
const guild = this.storage.getGuild(msg.guild);
|
||||
|
||||
let server: DBServer = this.storage.db.getServer(msg.guild);
|
||||
let user: DBUser = server.getUser(msg.member);
|
||||
|
||||
const time = Date.now();
|
||||
user.messages++;
|
||||
|
||||
let xp = Math.round(Math.random() + Math.min(msg.content.length / 70, 3.0) * 100) / 100;
|
||||
let thankedTheDog = false;
|
||||
|
||||
if (msg.content.toLowerCase().substr(0, 8) == "good dog") {
|
||||
// Thank the dog
|
||||
msg.channel.fetchMessages({ limit: 2 }).then(messages => {
|
||||
let lastMsg = messages.last();
|
||||
if (lastMsg.author.id == this.client.user.id && lastMsg.attachments.first()) {
|
||||
let filename = lastMsg.attachments.first().filename;
|
||||
let user = filename.substr(0, filename.length - 4);
|
||||
|
||||
if (user == msg.member.id) {
|
||||
msg.reply("woof!");
|
||||
xp += Math.random() * 6;
|
||||
thankedTheDog = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (/good.(bo[i|y]|g[u|i]rl)/gi.test(msg.content.toLowerCase())) {
|
||||
// Make dog sad
|
||||
msg.channel.fetchMessages({ limit: 2 }).then(messages => {
|
||||
let lastMsg = messages.last();
|
||||
if (lastMsg.author.id == this.client.user.id && lastMsg.attachments.first()) {
|
||||
let filename = lastMsg.attachments.first().filename;
|
||||
let user = filename.substr(0, filename.length - 4);
|
||||
|
||||
if (user == msg.member.id) {
|
||||
msg.reply("I'm enby tho :(");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (let voice in guild.chatChannels) {
|
||||
if (guild.chatChannels[voice] == msg.channel.id) {
|
||||
xp /= 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore the first message in a while to prevent single spam messages gaining XP
|
||||
if (!thankedTheDog && (!user.lastInstigated || time - user.lastInstigated >= 300 * 1000)) {
|
||||
user.lastInstigated = time;
|
||||
//Don't score the message if it is less than 30 chars long (a "useless" message)
|
||||
if (msg.content.length < 30) {
|
||||
server.pushUser(user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only count messages every 30 seconds
|
||||
if (thankedTheDog || (time - user.lastPosted >= 30 * 1000)) {
|
||||
user.lastPosted = time;
|
||||
user.lastInstigated = time;
|
||||
|
||||
user.levelXP += xp;
|
||||
user.totalXP += xp;
|
||||
|
||||
const cost = (this.storage.conf.xp_properties.level_base_cost + Math.pow(user.level, this.storage.conf.xp_properties.level_multiplier));
|
||||
|
||||
if (user.levelXP >= cost) {
|
||||
user.level++;
|
||||
user.levelXP -= cost;
|
||||
|
||||
this.imgBuilder.generate(msg.member.displayName, user.level, msg.author.id).then(image => {
|
||||
msg.channel.send("", {file: image as any}).then(() => {
|
||||
fs.unlinkSync(image);
|
||||
});
|
||||
});
|
||||
|
||||
let currentRole = -1;
|
||||
let previousRole = -1;
|
||||
|
||||
const roles: BotLevelRoles = server.getLevelRolesTable();
|
||||
|
||||
for (let role in roles) {
|
||||
let num = parseInt(role);
|
||||
if (num <= user.level && num > currentRole) currentRole = num;
|
||||
if (num <= user.level - 1 && num > previousRole) previousRole = num;
|
||||
}
|
||||
|
||||
if (currentRole != previousRole) {
|
||||
if (previousRole != -1) msg.member.removeRole(msg.guild.roles.find(r => r.id == roles[previousRole]));
|
||||
if (currentRole != -1) msg.member.addRole(msg.guild.roles.find(r => r.id == roles[currentRole]), 'Update user level role.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server.pushUser(user);
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
clearInterval(this.checkVAInterval);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import * as Discord from 'discord.js';
|
||||
import { LevelPluginGuild, LevelPluginUser } from './LevelPlugin';
|
||||
|
||||
export default class LevelCommand {
|
||||
constructor(private roles: { name: string, experience: number, total_experience: number, role: number }[]) {}
|
||||
|
||||
async trigger(msg: Discord.Message) {
|
||||
if (!msg.guild) return;
|
||||
const guild = await LevelPluginGuild.findOne({ id: msg.guild.id });
|
||||
if (!guild) return;
|
||||
|
||||
const users = (await LevelPluginUser.find({ guild_id: guild._id }).sort({ level: 'desc' }).limit(12)).filter(u => u.id);
|
||||
|
||||
const embed = new Discord.MessageEmbed()
|
||||
.setAuthor("Leaderboard", "https://i.imgur.com/LaPvO6n.png")
|
||||
.setColor("#FFAC38")
|
||||
.setDescription(`The most active members in ${msg.guild.name}.`)
|
||||
.setFooter(`Requested by ${msg.member!.displayName}`, msg.author.avatarURL({ size: 32 })!)
|
||||
.setTimestamp();
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
try {
|
||||
let name = (await msg.guild.members.fetch(users[i].id ?? "0")).displayName;
|
||||
let currentRole = null;
|
||||
for (let role of this.roles) {
|
||||
if (role.total_experience < users[i].experience) currentRole = role;
|
||||
else break;
|
||||
}
|
||||
|
||||
if (name.length >= 20) name = name.substr(0, 18) + "...";
|
||||
embed.addField(
|
||||
`⠀${i < 3 ? "**" : ""}${i + 1}) ${name}${i < 3 ? "**" : ""}`,
|
||||
`⠀Level ${currentRole?.name ?? 'Potato'} • ${Math.floor(users[i].experience ?? 0)} XP`, true);
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
msg.channel.send({ embed }).catch(_ => { /* Missing send permissions. */ });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import { LevelPluginGuild, LevelPluginUser } from './LevelPlugin';
|
||||
|
||||
export default class LevelCommand {
|
||||
constructor(private roles: { name: string, experience: number, total_experience: number, role: number }[]) {}
|
||||
|
||||
async trigger(msg: Discord.Message) {
|
||||
if (!msg.guild) return;
|
||||
const guild = await LevelPluginGuild.findOne({ id: msg.guild.id });
|
||||
if (!guild) return;
|
||||
const user = (await LevelPluginUser.findOne({ guild_id: guild._id, id: msg.author.id })) ??
|
||||
{ experience: 0, level: 0 };
|
||||
|
||||
let currentRole = null;
|
||||
for (let role of this.roles) {
|
||||
if (role.total_experience < user.experience) currentRole = role;
|
||||
else break;
|
||||
}
|
||||
const nextRole = currentRole ? this.roles[this.roles.indexOf(currentRole) + 1] : this.roles[0];
|
||||
|
||||
msg.channel.send({
|
||||
embed: new Discord.MessageEmbed()
|
||||
.setAuthor("My Level", "https://i.imgur.com/Nqyb94h.png")
|
||||
.setColor("#15B5A6")
|
||||
.setDescription(`Statistics for ${msg.member!.displayName} in ${msg.guild!.name}.`)
|
||||
.setFooter(`Requested by ${msg.member!.displayName}`, msg.author.avatarURL({ size: 16 })!)
|
||||
.setTimestamp()
|
||||
|
||||
.addField(`⠀Level`, `⠀${user.level}`, true)
|
||||
.addField(`⠀Experience`, `⠀${Math.floor(user.experience - (currentRole?.total_experience ?? 0))} ` +
|
||||
(nextRole ? '/ ' + nextRole.experience : ''), true)
|
||||
.addField(`⠀Rank`, `⠀${currentRole ? currentRole.name : 'Potato'}`, true)
|
||||
}).catch(_ => { /* Missing send permissions. */ });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
import * as Mongoose from 'mongoose';
|
||||
import * as Discord from 'discord.js';
|
||||
|
||||
import { BotConfig } from '../../Bot';
|
||||
import { Command, CommandFn } from '../../Commands/Command';
|
||||
|
||||
import LevelCommand from './LevelCommand';
|
||||
import LeaderboardCommand from './LeaderboardCommand';
|
||||
|
||||
interface LevelPluginConfig {
|
||||
please_and_thank_you: boolean;
|
||||
message: {
|
||||
cooldown: number;
|
||||
min_length: number;
|
||||
}
|
||||
levels: {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
experience: number;
|
||||
role: number;
|
||||
}
|
||||
}
|
||||
}
|
||||
// import {GuildData} from "../GuildData";
|
||||
// import {LevelImageBuilder} from "./LevelImageBuilder"
|
||||
// import {BotLevelRoles, Database, DBServer, DBUser} from "../Database";
|
||||
|
||||
const levelPluginGuildSchema = new Mongoose.Schema({
|
||||
id: String
|
||||
});
|
||||
|
||||
interface ILevelPluginGuild extends Mongoose.Document {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const LevelPluginGuild = Mongoose.model<ILevelPluginGuild>('LevelPluginGuild', levelPluginGuildSchema);
|
||||
|
||||
const levelPluginUserSchema = new Mongoose.Schema({
|
||||
guild_id: String,
|
||||
id: String,
|
||||
|
||||
level: Number,
|
||||
experience: Number,
|
||||
totalMessages: Number,
|
||||
lastInteracted: Number
|
||||
});
|
||||
|
||||
interface ILevelPluginUser extends Mongoose.Document {
|
||||
guild_id: ILevelPluginGuild['_id'];
|
||||
id: string;
|
||||
|
||||
level: number;
|
||||
experience: number;
|
||||
totalMessages: number;
|
||||
lastInteracted: number;
|
||||
}
|
||||
|
||||
export const LevelPluginUser = Mongoose.model<ILevelPluginUser>('LevelPluginUser', levelPluginUserSchema);
|
||||
|
||||
export default class LevelPlugin {
|
||||
private roles: { name: string, experience: number, total_experience: number, role: number }[] = [];
|
||||
// storage: BotStorage;
|
||||
// imgBuilder: LevelImageBuilder;
|
||||
// checkVAInterval: any;
|
||||
|
||||
constructor(private config: BotConfig & { plugin: { level: LevelPluginConfig } },
|
||||
private client: Discord.Client, commands: { [command: string]: Command | CommandFn }) {
|
||||
// this.storage = storage;
|
||||
// this.imgBuilder = new LevelImageBuilder();
|
||||
|
||||
let total_experience = 0;
|
||||
Object.keys(this.config.plugin.level.levels).map(m => parseInt(m)).sort((a, b) => a - b).forEach(n => {
|
||||
const role = this.config.plugin.level.levels[n.toString()];
|
||||
total_experience += role.experience;
|
||||
this.roles.push({ ...role, total_experience });
|
||||
});
|
||||
|
||||
client.on('message', this.onMessage);
|
||||
commands.level = new LevelCommand(this.roles);
|
||||
commands.leaderboard = new LeaderboardCommand(this.roles);
|
||||
|
||||
// this.checkVAInterval = setInterval(this.checkVoiceActivity.bind(this), 5*1000*60);
|
||||
}
|
||||
|
||||
private onMessage = async (msg: Discord.Message) => {
|
||||
if (msg.author.id === this.client.user!.id) return;
|
||||
if (msg.content.substr(0, this.config.options.prefix.length + 1).toLowerCase() == this.config.options.prefix + ' ') return;
|
||||
|
||||
// Completely ignore messages that are less than N characters and without a space.
|
||||
if (msg.content.length < this.config.plugin.level.message.min_length || msg.content.split(' ').length - 1 < 1) return;
|
||||
|
||||
// Ignore DM conversations.
|
||||
if (!msg.guild) return;
|
||||
|
||||
const { _id: guild_id } = await LevelPluginGuild.findOneAndUpdate({ id: msg.guild.id },
|
||||
{ $setOnInsert: { id: msg.guild.id } },
|
||||
{ upsert: true, new: true });
|
||||
|
||||
const user = await LevelPluginUser.findOneAndUpdate({ id: msg.author.id, guild_id }, {
|
||||
$setOnInsert: {
|
||||
guild_id: guild_id,
|
||||
id: msg.author.id,
|
||||
experience: 0,
|
||||
level: 0,
|
||||
},
|
||||
$inc: { totalMessages: 1 }
|
||||
}, { upsert: true, new: true });
|
||||
|
||||
let experience = Math.round(Math.random() + Math.min(msg.content.length / 70, 3.0) * 100) / 100;
|
||||
let thanked = false;
|
||||
|
||||
// Allow people to thank the dog... allow the dog to feel emotion.
|
||||
if (this.config.plugin.level.please_and_thank_you && msg.content.toLowerCase().startsWith('good')) {
|
||||
const lastMsg = (await msg.channel.messages.fetch({ limit: 2 })).last();
|
||||
if (lastMsg && lastMsg.author.id !== this.client.user!.id) {
|
||||
if (/good.(bo[i|y]|g[u|i]rl)/gi.test(msg.content.toLowerCase())) {
|
||||
msg.reply("I'm enby tho :(");
|
||||
return;
|
||||
}
|
||||
else if (msg.content.toLowerCase().startsWith('good dog') &&
|
||||
(lastMsg.attachments.first()?.name || '').substring(0, msg.member!.id.length) === msg.member!.id) {
|
||||
msg.reply('Woof!');
|
||||
experience += Math.random() * 6;
|
||||
thanked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore messages that are too recent.
|
||||
if (!thanked && (Date.now() - user.lastInteracted < this.config.plugin.level.message.cooldown * 1000)) return;
|
||||
|
||||
await LevelPluginUser.findOneAndUpdate({ id: msg.author.id, guild_id }, {
|
||||
$inc: { experience },
|
||||
$set: { lastInteracted: Date.now() }
|
||||
}, { new: true });
|
||||
|
||||
// for (let voice in guild.chatChannels) {
|
||||
// if (guild.chatChannels[voice] == msg.channel.id) {
|
||||
// xp /= 3;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// const cost = (this.storage.conf.xp_properties.level_base_cost + Math.pow(user.level, this.storage.conf.xp_properties.level_multiplier));
|
||||
|
||||
// if (user.levelXP >= cost) {
|
||||
// user.level++;
|
||||
// user.levelXP -= cost;
|
||||
|
||||
// this.imgBuilder.generate(msg.member.displayName, user.level, msg.author.id).then(image => {
|
||||
// msg.channel.send("", {file: image as any}).then(() => {
|
||||
// fs.unlinkSync(image);
|
||||
// });
|
||||
// });
|
||||
|
||||
// let currentRole = -1;
|
||||
// let previousRole = -1;
|
||||
|
||||
// const roles: BotLevelRoles = server.getLevelRolesTable();
|
||||
|
||||
// for (let role in roles) {
|
||||
// let num = parseInt(role);
|
||||
// if (num <= user.level && num > currentRole) currentRole = num;
|
||||
// if (num <= user.level - 1 && num > previousRole) previousRole = num;
|
||||
// }
|
||||
|
||||
// if (currentRole != previousRole) {
|
||||
// if (previousRole != -1) msg.member.removeRole(msg.guild.roles.find(r => r.id == roles[previousRole]));
|
||||
// if (currentRole != -1) msg.member.addRole(msg.guild.roles.find(r => r.id == roles[currentRole]), 'Update user level role.');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// checkVoiceActivity(): void {
|
||||
// for (let guildKey in this.storage.guildData) {
|
||||
// let guild = this.storage.guildData[guildKey];
|
||||
// for (let activeVoiceChannel in guild.chatChannels) {
|
||||
// let channel = guild.guild.channels.get(activeVoiceChannel) as Discord.VoiceChannel;
|
||||
// let undeafenedUsers: number = 0;
|
||||
|
||||
// channel.members.forEach((member, key) => {
|
||||
// if (!member.selfDeaf) undeafenedUsers++;
|
||||
// });
|
||||
|
||||
// if (undeafenedUsers >= 2) {
|
||||
// channel.members.forEach((member, key) => {
|
||||
// if (!member.selfDeaf && !member.selfMute) {
|
||||
|
||||
// let chatChannel = (guild.guild.channels.get(guild.chatChannels[activeVoiceChannel]) as Discord.TextChannel);
|
||||
|
||||
// let server: DBServer = this.storage.db.getServer(guild.guild);
|
||||
// let user: DBUser = server.getUser(member);
|
||||
|
||||
// let xp = Math.round(Math.random() + 0.3);
|
||||
|
||||
// user.levelXP += xp;
|
||||
// user.totalXP += xp;
|
||||
|
||||
// const cost = (this.storage.conf.xp_properties.level_base_cost + Math.pow(user.level, this.storage.conf.xp_properties.level_multiplier));
|
||||
|
||||
// if (user.levelXP >= cost) {
|
||||
// user.level++;
|
||||
// user.levelXP -= cost;
|
||||
|
||||
// if (chatChannel) {
|
||||
// this.imgBuilder.generate(member.displayName, user.level, member.id).then(image => {
|
||||
// chatChannel.send("", {file: image as any}).then(() => {
|
||||
// fs.unlinkSync(image);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// let currentRole = -1;
|
||||
// let previousRole = -1;
|
||||
|
||||
// const roles: BotLevelRoles = server.getLevelRolesTable();
|
||||
|
||||
// for (let role in roles) {
|
||||
// let num = parseInt(role);
|
||||
// if (num <= user.level && num > currentRole) currentRole = num;
|
||||
// if (num <= user.level - 1 && num > previousRole) previousRole = num;
|
||||
// }
|
||||
|
||||
// if (currentRole != previousRole) {
|
||||
// if (previousRole != -1) member.removeRole(member.guild.roles.find(r => r.id == roles[previousRole]));
|
||||
// if (currentRole != -1) member.addRole(member.guild.roles.find(r => r.id == roles[currentRole]), 'Update user level role.');
|
||||
// }
|
||||
// }
|
||||
|
||||
// server.pushUser(user);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import * as Discord from 'discord.js';
|
||||
|
||||
import { BotConfig } from '../../Bot';
|
||||
|
||||
interface VoiceChatPluginConfig {
|
||||
description: {
|
||||
prefix?: string,
|
||||
suffix?: string
|
||||
}
|
||||
channel: {
|
||||
prefix?: string,
|
||||
suffix?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default class VoiceChatPlugin {
|
||||
private channels: { [guild: string]: { [voice_id: string]: string } } = {};
|
||||
|
||||
private description_prefix = "**Temporary discussion for ";
|
||||
private description_suffix = ".**"
|
||||
private channel_prefix = "";
|
||||
private channel_suffix = "-chat"
|
||||
|
||||
constructor(config: BotConfig & { plugin: { voice_chat: VoiceChatPluginConfig } }, private client: Discord.Client) {
|
||||
client.on("voiceStateUpdate", this.onVoiceStateUpdate);
|
||||
|
||||
if (config.plugin?.voice_chat?.description?.prefix) this.description_prefix = config.plugin.voice_chat.description.prefix;
|
||||
if (config.plugin?.voice_chat?.description?.suffix) this.description_suffix = config.plugin.voice_chat.description.suffix;
|
||||
if (config.plugin?.voice_chat?.channel?.prefix) this.channel_prefix = config.plugin.voice_chat.channel.prefix;
|
||||
if (config.plugin?.voice_chat?.channel?.suffix) this.channel_suffix = config.plugin.voice_chat.channel.suffix;
|
||||
}
|
||||
|
||||
onVoiceStateUpdate = (oldState: Discord.VoiceState, newState: Discord.VoiceState) => {
|
||||
if (oldState.channelID == newState.channelID) return;
|
||||
if (oldState.channelID != null) {
|
||||
let channel = oldState.guild.channels.resolve(oldState.channelID);
|
||||
if (!channel) return;
|
||||
if (channel.members.size == 0 && this.channels[channel.guild.id]) {
|
||||
oldState.guild.channels.resolve(this.channels[channel.guild.id][channel.id])?.delete();
|
||||
delete this.channels[channel.guild.id][channel.id];
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.channelID != null) {
|
||||
let channel = newState.guild.channels.resolve(newState.channelID);
|
||||
if (!channel) return;
|
||||
if (!this.channels[channel.guild.id]?.[channel.id] && channel.members.size == 1 && channel.parent) this.createChatChannel(channel as any);
|
||||
}
|
||||
}
|
||||
|
||||
createChatChannel(voice: Discord.VoiceChannel) {
|
||||
let channelName = this.channel_prefix + voice.name.replace(/[\W_]+/g,"-").replace(/-+/g, "-") + this.channel_suffix;
|
||||
|
||||
voice.guild.channels.create(channelName, {
|
||||
type: `text`,
|
||||
topic: `${this.description_prefix}<#${voice.id}>${this.description_suffix}`,
|
||||
parent: voice.parent ?? undefined
|
||||
}).then(channel => {
|
||||
if (!this.channels[channel.guild.id]) this.channels[channel.guild.id] = {};
|
||||
this.channels[channel.guild.id][voice.id] = channel.id;
|
||||
channel.send({
|
||||
embed: new Discord.MessageEmbed()
|
||||
.setAuthor(channelName, "https://i.imgur.com/vitVUtr.png")
|
||||
.setColor("#EE86ED")
|
||||
.setDescription(
|
||||
`This is a temporary discussion channel for #${voice.name}!\n` +
|
||||
`This channel will be automatically deleted when everybody leaves the voice channel.\n`)
|
||||
.setTimestamp()
|
||||
}).catch(_ => { /* Missing send permissions. */ });
|
||||
}).catch(_ => { /* Channel was removed. */ });
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
await Promise.all(Object.entries(this.channels).map(async ([g, channels]) => {
|
||||
let guild = await this.client.guilds.fetch(g);
|
||||
return await Promise.all(Object.values(channels).map(async channel =>
|
||||
(await guild.channels.resolve(channel))?.delete()));
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -1,16 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"rootDirs": ["./src/",],
|
||||
"typeRoots": ["./node_modules/@types/"],
|
||||
"outDir": "./build",
|
||||
"alwaysStrict": true
|
||||
},
|
||||
"include": [
|
||||
"./src/*",
|
||||
"./src/**/*"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build",
|
||||
"typeRoots": [ "./node_modules/@types/" ],
|
||||
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
|
||||
"removeComments": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue