UNPKG

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
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; } }