willow-plugin-exp
Version:
A plugin for willow that allows users to collect exp, gain levels, and be assigned roles.
291 lines (272 loc) • 12.1 kB
text/typescript
import 'reflect-metadata';
import commandLineArgs, { CommandLineOptions } from 'command-line-args';
import * as Discord from 'discord.js';
import _ from 'lodash';
import moment from 'moment';
import { BotORMOptions, BotPlugin } from 'willow-contracts';
import { Guild, GuildUser, PluginExpUser, UserExp, UserLevel, UserRank } from 'willow-models';
import { Between, createConnection, Repository } from 'typeorm';
export const commandOptions = [
{ name: 'exp', alias: 'e', type: Boolean },
{ name: 'rank', alias: 'r', type: Boolean },
{ name: 'help', alias: 'h', type: Boolean }
];
export enum CommandType { Quote, Random }
export default class PsyduckExampleOrm implements BotPlugin {
public users!: Repository<PluginExpUser>;
public levels!: Repository<UserLevel>;
public ranks!: Repository<UserRank>;
public exps!: Repository<UserExp>;
public connectionOptions: BotORMOptions;
public announceLevelUp: { announceChannel: string } | boolean;
public minExp: number;
public maxExp: number;
constructor(options: BotORMOptions, announceLevelUp: { announceChannel: string } | boolean = false, minExp: number = 1, maxExp: number = 10) {
this.connectionOptions = options;
this.announceLevelUp = announceLevelUp;
this.minExp = minExp;
this.maxExp = maxExp;
}
/**
* This method is to be called after instantiation to connect to the data store. This is intentional to allow for unit testing methods without requiring a connection.
*/
connect() {
createConnection({
type: this.connectionOptions.type as any, // This is a workaround for TypeORM. Although the interfaces match, it is checking design time values.
host: this.connectionOptions.host,
port: this.connectionOptions.port,
username: this.connectionOptions.username,
password: this.connectionOptions.password,
database: this.connectionOptions.database,
synchronize: true,
logging: false,
entities: [
PluginExpUser, UserLevel, UserExp, UserRank
]
}).then(async connection => {
this.users = connection.getRepository(PluginExpUser);
this.levels = connection.getRepository(UserLevel);
this.ranks = connection.getRepository(UserRank);
this.exps = connection.getRepository(UserExp);
this.buildLevels();
}).catch(error => console.log(error));
}
async buildLevels() {
const levels = await this.levels.find();
if (!levels || levels.length < 1) {
for (let i = 0; i < 100; i++) {
const level = new UserLevel();
level.level = i + 1;
level.requiredExp = (i + 1) * 1993 - 1185;
await this.levels.save(level);
}
}
}
/**
* This event is emitted from Psyduck when a message is recieved in a discord channel. From there you may do as you with with it in your plugin.
*/
onMessage(message: Discord.Message) {
// If the message came from a bot, ignore.
if (message.author.bot) {
return;
}
// If the message is too small to be a command, ignore.
if (message.content.length <= 1) {
this.gainExp(message, message.author.id, (message.channel as Discord.TextChannel).name);
return;
}
// If the message is a string of repeated characters, ignore. For example !!!!!!!
const dupe = RegExp(/^(.)\1*$/gm);
if (dupe.test(message.content)) {
this.gainExp(message, message.author.id, (message.channel as Discord.TextChannel).name);
return;
}
// Extract the command code, if it is in the list of commands we want to handle, continue.
if (message.content.startsWith('!')) {
const commandCodes = ['level', 'lvl', 'l'];
const baseCommand = message.content.slice(1);
const commandWithArgs = message.content.slice(1, message.content.indexOf(' '));
if (_.includes(commandCodes, baseCommand)) {
this.explainLevel(message, (message.channel as Discord.TextChannel).name);
}
else if (_.includes(commandCodes, commandWithArgs)) {
}
}
else {
this.gainExp(message, message.author.id, (message.channel as Discord.TextChannel).name);
}
}
async getUser(userId: string) {
return await this.users.findOne({
user: {
id: userId
}
}, { relations: ['exps', 'level'] });
}
createUser(guildId: string, serverName: string, guildAvatarUrl: string, userId: string, userName: string, avatarUrl: string) {
const user = new PluginExpUser();
const guildUser = new GuildUser();
guildUser.id = userId;
guildUser.userName = userName;
guildUser.avatarUrl = avatarUrl || '';
const guild = new Guild();
guild.id = guildId;
guild.serverName = serverName;
guild.avatarUrl = guildAvatarUrl || '';
guildUser.guild = guild;
user.user = guildUser;
user.exps = [];
return user;
}
async explainLevel(discord: Discord.Message, channelName: string) {
let user = await this.getUser(discord.author.id);
if (!user) {
user = this.createUser(discord.guild.id, discord.guild.name, discord.guild.iconURL, discord.author.id, discord.author.username, discord.author.avatarURL);
user.exps.push(this.buildExp(channelName));
await this.users.save(user);
}
let users = await this.users.find({ relations: ['level', 'exps'] });
users = _.orderBy(users, u => _.sum(u.exps.map(e => e.amount)), ['desc']);
let currentExp = 0;
user.exps.forEach(exp => currentExp += exp.amount);
const nextLevel = await this.levels.findOne({ level: user.level ? user.level.level + 1 : 1 });
const userIndex = users.findIndex(u => u.user.id === user!.user.id);
const nextPlaceUser = users[userIndex === 0 ? 0 : userIndex - 1];
let nextPlaceUserExp = 0;
let nextPlaceUserName = undefined;
if (nextPlaceUser) {
try {
const du = await discord.guild.fetchMember(nextPlaceUser.user.id);
nextPlaceUserName = du.user.username;
} catch (error) {
console.log(error);
}
nextPlaceUser.exps.forEach(exp => nextPlaceUserExp += exp.amount);
}
const embed = {
'color': 11812284,
'author': {
'name': '💯 Your Level Report'
},
'fields': [
{
'name': 'Overall Rank',
'value': `${userIndex + 1}/${users.length}`,
'inline': true
},
{
'name': 'Current Exp',
'value': `${currentExp}`,
'inline': true
},
{
'name': 'Current Level',
'value': `**${user.level ? user.level.level : 'None'}** (req. *${user.level ? user.level.requiredExp : 0} exp*)`,
'inline': true
},
{
'name': 'Next Level',
'value': `**${nextLevel ? nextLevel.level : 'none'}** (req. *${nextLevel ? nextLevel.requiredExp : 0} exp*)`,
'inline': true
},
{
'name': 'Exp To Next Level',
'value': `${nextLevel ? nextLevel.requiredExp - currentExp : 0}`,
'inline': true
},
{
'name': 'Exp To Overtake',
'value': `${nextPlaceUserExp - currentExp} (*${nextPlaceUserName || nextPlaceUser.user.id}*)`,
'inline': true
}
]
};
discord.channel.send({ embed });
}
buildExp(channel: string): UserExp {
const exp = new UserExp();
exp.channel = channel;
exp.timeStamp = moment().format('ddd MMM DD YYYY h:mm A');
exp.amount = _.random(this.minExp, this.maxExp);
return exp;
}
async gainExp(discord: Discord.Message, userId: string, channel: string) {
let user = await this.getUser(userId);
if (!user) {
user = this.createUser(discord.guild.id, discord.guild.name, discord.guild.iconURL, discord.author.id, discord.author.username, discord.author.avatarURL);
await this.users.save(user);
}
user.exps.push(this.buildExp(channel));
await this.users.save(user);
this.checkGainLevel(discord, user);
}
async checkGainLevel(discord: Discord.Message, user: PluginExpUser) {
let currentExp = 0;
let leveled = false;
user.exps.forEach(exp => currentExp += exp.amount);
const levels = await this.levels.find({ requiredExp: Between(1, currentExp) });
if (!user.level) {
leveled = true;
user.level = levels[0];
await this.users.save(user);
} else {
for (let i = 0; i < levels.length; i++) {
const level = levels[i];
if (level.id > user.level.id) {
leveled = true;
user.level = level;
await this.users.save(user);
break;
}
}
}
await this.users.save(user);
if (leveled) {
await this.gainLevel(discord, user);
if (this.announceLevelUp) {
if (typeof (this.announceLevelUp) === 'boolean') {
discord.channel.send(`Level 🆙 <@${user.user.id}> - You are now level ${user.level.level}`);
} else {
const channel = discord.guild.channels.find('id', this.announceLevelUp.announceChannel) as Discord.TextChannel;
if (channel) {
channel.send(`Level 🆙 <@${user.user.id}> - You are now level ${user.level.level}`);
}
}
}
}
}
async gainLevel(discord: Discord.Message, user: PluginExpUser) {
const currentLevel = await this.levels.findOne({ id: user.level.id }, { relations: ['ranks'] });
if (currentLevel) {
currentLevel.ranks.forEach(r => {
this.assignRole(discord, user.user.id, r.rankName);
});
}
}
async assignRole(discord: Discord.Message, userId: string, roleName: string) {
try {
const serverRole = discord.guild.roles.find('name', roleName);
if (serverRole) {
const guildUser = await discord.guild.fetchMember(userId);
if (guildUser && !guildUser.roles.get(serverRole.id)) {
guildUser.addRole(serverRole);
}
}
} catch (error) {
}
}
parseParameters(messageContent: string): string[] {
const params = messageContent.match(/ (?=\S)[^'\s]*(?:'[^\\']*(?:\\[\s\S][^\\']*)*'[^'\s]*)*/g) || [];
return params.map(p => p.trim());
}
hydrateOptions(parameters: string[]): CommandLineOptions {
return commandLineArgs(commandOptions, { argv: parameters, partial: true });
}
extractUserId(userId: string): string | undefined {
const matches = userId.match(/(?<=@|!)(.*)(?=>)/gm);
if (matches && matches.length === 1) {
return matches[0].replace('!', '');
}
return undefined;
}
}