├── .gitattributes ├── docker.sh ├── assets ├── weather │ ├── base │ │ ├── day.png │ │ ├── rain.png │ │ ├── snow.png │ │ ├── cloudy.png │ │ ├── night.png │ │ ├── windy.png │ │ └── thunderstorm.png │ ├── icons │ │ ├── dark │ │ │ ├── fog.png │ │ │ ├── rain.png │ │ │ ├── sleet.png │ │ │ ├── snow.png │ │ │ ├── wind.png │ │ │ ├── cloudy.png │ │ │ ├── flurries.png │ │ │ ├── humidity.png │ │ │ ├── pointer.png │ │ │ ├── precip.png │ │ │ ├── unknown.png │ │ │ ├── clear-day.png │ │ │ ├── clear-night.png │ │ │ ├── thunderstorm.png │ │ │ ├── partly-cloudy-day.png │ │ │ └── partly-cloudy-night.png │ │ └── light │ │ │ ├── fog.png │ │ │ ├── rain.png │ │ │ ├── snow.png │ │ │ ├── wind.png │ │ │ ├── cloudy.png │ │ │ ├── pointer.png │ │ │ ├── precip.png │ │ │ ├── sleet.png │ │ │ ├── unknown.png │ │ │ ├── clear-day.png │ │ │ ├── flurries.png │ │ │ ├── humidity.png │ │ │ ├── clear-night.png │ │ │ ├── thunderstorm.png │ │ │ ├── partly-cloudy-day.png │ │ │ └── partly-cloudy-night.png │ └── fonts │ │ ├── Roboto-Regular.ttf │ │ ├── RobotoMono-Light.ttf │ │ └── RobotoCondensed-Regular.ttf └── profile │ ├── fonts │ ├── Roboto.ttf │ └── NotoEmoji-Regular.ttf │ └── backgrounds │ └── default.png ├── structures ├── currency │ ├── StoreItem.js │ ├── ItemGroup.js │ ├── Store.js │ ├── Daily.js │ ├── Experience.js │ ├── Inventory.js │ ├── Bank.js │ └── Currency.js ├── CommandoClient.js ├── Redis.js ├── games │ ├── RussianRoulette.js │ ├── Blackjack.js │ └── Roulette.js ├── PostgreSQL.js └── Song.js ├── models ├── Item.js ├── UserName.js ├── Tag.js └── UserProfile.js ├── .dockerignore ├── .travis.yml ├── .gitignore ├── util └── Util.js ├── commands ├── tags │ ├── tag.js │ ├── source.js │ ├── list-all.js │ ├── info.js │ ├── list.js │ ├── add.js │ ├── add-server.js │ ├── delete.js │ └── add-example.js ├── economy │ ├── unlock-all.js │ ├── bank.js │ ├── lock-all.js │ ├── add.js │ ├── lock.js │ ├── remove.js │ ├── daily-random.js │ ├── unlock.js │ ├── daily.js │ ├── deposit.js │ ├── money.js │ ├── withdraw.js │ ├── trade.js │ └── leaderboard.js ├── music │ ├── stop.js │ ├── pause.js │ ├── resume.js │ ├── default-volume.js │ ├── save.js │ ├── status.js │ ├── max-songs.js │ ├── max-length.js │ ├── volume.js │ ├── queue.js │ └── skip.js ├── info │ ├── about.js │ ├── server-info.js │ └── user-info.js ├── util │ ├── blacklist-user.js │ ├── whitelist-user.js │ ├── remindme.js │ ├── stats.js │ ├── search.js │ ├── strawpoll.js │ ├── translate.js │ ├── clean.js │ └── cybernuke.js ├── item │ ├── info.js │ ├── store.js │ ├── add.js │ ├── inventory.js │ ├── give.js │ ├── buy.js │ └── trade.js ├── games │ ├── roulette-info.js │ ├── russian-roulette.js │ ├── roulette.js │ ├── slot-machine.js │ └── blackjack.js ├── social │ ├── personal-message.js │ ├── blame.js │ ├── please.js │ └── profile.js ├── weather │ └── weather.js └── docs │ └── docs.js ├── Dockerfile ├── LICENSE ├── types └── emoji.js ├── deploy └── deploy.sh ├── README.md ├── docker-compose.yml.example ├── package.json ├── Commando.js └── providers └── Sequelize.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose down 4 | docker-compose pull 5 | docker-compose up -d 6 | -------------------------------------------------------------------------------- /assets/weather/base/day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/base/day.png -------------------------------------------------------------------------------- /assets/weather/base/rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/base/rain.png -------------------------------------------------------------------------------- /assets/weather/base/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/base/snow.png -------------------------------------------------------------------------------- /assets/weather/base/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/base/cloudy.png -------------------------------------------------------------------------------- /assets/weather/base/night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/base/night.png -------------------------------------------------------------------------------- /assets/weather/base/windy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/base/windy.png -------------------------------------------------------------------------------- /assets/profile/fonts/Roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/profile/fonts/Roboto.ttf -------------------------------------------------------------------------------- /assets/weather/icons/dark/fog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/fog.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/rain.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/sleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/sleet.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/snow.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/wind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/wind.png -------------------------------------------------------------------------------- /assets/weather/icons/light/fog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/fog.png -------------------------------------------------------------------------------- /assets/weather/icons/light/rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/rain.png -------------------------------------------------------------------------------- /assets/weather/icons/light/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/snow.png -------------------------------------------------------------------------------- /assets/weather/icons/light/wind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/wind.png -------------------------------------------------------------------------------- /assets/profile/backgrounds/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/profile/backgrounds/default.png -------------------------------------------------------------------------------- /assets/weather/base/thunderstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/base/thunderstorm.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/cloudy.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/flurries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/flurries.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/humidity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/humidity.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/pointer.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/precip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/precip.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/unknown.png -------------------------------------------------------------------------------- /assets/weather/icons/light/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/cloudy.png -------------------------------------------------------------------------------- /assets/weather/icons/light/pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/pointer.png -------------------------------------------------------------------------------- /assets/weather/icons/light/precip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/precip.png -------------------------------------------------------------------------------- /assets/weather/icons/light/sleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/sleet.png -------------------------------------------------------------------------------- /assets/weather/icons/light/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/unknown.png -------------------------------------------------------------------------------- /assets/weather/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /assets/weather/icons/dark/clear-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/clear-day.png -------------------------------------------------------------------------------- /assets/weather/icons/light/clear-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/clear-day.png -------------------------------------------------------------------------------- /assets/weather/icons/light/flurries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/flurries.png -------------------------------------------------------------------------------- /assets/weather/icons/light/humidity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/humidity.png -------------------------------------------------------------------------------- /assets/profile/fonts/NotoEmoji-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/profile/fonts/NotoEmoji-Regular.ttf -------------------------------------------------------------------------------- /assets/weather/fonts/RobotoMono-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/fonts/RobotoMono-Light.ttf -------------------------------------------------------------------------------- /assets/weather/icons/dark/clear-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/clear-night.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/thunderstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/thunderstorm.png -------------------------------------------------------------------------------- /assets/weather/icons/light/clear-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/clear-night.png -------------------------------------------------------------------------------- /assets/weather/icons/light/thunderstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/thunderstorm.png -------------------------------------------------------------------------------- /assets/weather/fonts/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/fonts/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /assets/weather/icons/dark/partly-cloudy-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/partly-cloudy-day.png -------------------------------------------------------------------------------- /assets/weather/icons/light/partly-cloudy-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/partly-cloudy-day.png -------------------------------------------------------------------------------- /assets/weather/icons/dark/partly-cloudy-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/dark/partly-cloudy-night.png -------------------------------------------------------------------------------- /assets/weather/icons/light/partly-cloudy-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeebDev/Commando/HEAD/assets/weather/icons/light/partly-cloudy-night.png -------------------------------------------------------------------------------- /structures/currency/StoreItem.js: -------------------------------------------------------------------------------- 1 | class StoreItem { 2 | constructor(name, price) { 3 | this.name = name; 4 | this.price = price; 5 | } 6 | } 7 | 8 | module.exports = StoreItem; 9 | -------------------------------------------------------------------------------- /structures/currency/ItemGroup.js: -------------------------------------------------------------------------------- 1 | class ItemGroup { 2 | constructor(item, amount) { 3 | this.item = item; 4 | this.amount = amount; 5 | } 6 | 7 | static convert(item, amount) { 8 | item = item.toLowerCase(); 9 | if (amount > 1 && /s$/.test(item)) return item.slice(0, -1); 10 | 11 | return item; 12 | } 13 | } 14 | 15 | module.exports = ItemGroup; 16 | -------------------------------------------------------------------------------- /models/Item.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | const Database = require('../structures/PostgreSQL'); 4 | 5 | const Item = Database.db.define('items', { 6 | name: { 7 | type: Sequelize.STRING, 8 | allowNull: false, 9 | unique: true 10 | }, 11 | price: { 12 | type: Sequelize.INTEGER, 13 | allowNull: false 14 | } 15 | }); 16 | 17 | module.exports = Item; 18 | -------------------------------------------------------------------------------- /models/UserName.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | const Database = require('../structures/PostgreSQL'); 4 | 5 | const UserName = Database.db.define('userName', { 6 | userID: Sequelize.STRING, 7 | username: Sequelize.STRING 8 | }, { 9 | indexes: [ 10 | { fields: ['userID'] }, 11 | { 12 | fields: ['userID', 'username'], 13 | unique: true 14 | } 15 | ] 16 | }); 17 | 18 | module.exports = UserName; 19 | -------------------------------------------------------------------------------- /structures/CommandoClient.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('discord.js-commando'); 2 | 3 | const Database = require('./PostgreSQL'); 4 | const Redis = require('./Redis'); 5 | 6 | class CommandoClient extends Client { 7 | constructor(options) { 8 | super(options); 9 | this.database = Database.db; 10 | this.redis = Redis.db; 11 | 12 | Database.start(); 13 | Redis.start(); 14 | } 15 | } 16 | 17 | module.exports = CommandoClient; 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # IDE 12 | .vscode 13 | 14 | # Git 15 | .github 16 | .git 17 | .gitattributes 18 | .gitignore 19 | 20 | # Deployment 21 | deploy 22 | .travis.yml 23 | 24 | # Dependency directory 25 | node_modules 26 | 27 | # Docker 28 | .dockerignore 29 | docker-compose.yml 30 | docker-compose.yml.example 31 | docker.sh 32 | Dockerfile 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | services: 4 | - docker 5 | addons: 6 | apt: 7 | sources: 8 | - git-core 9 | packages: 10 | - git 11 | before_install: 12 | - sudo apt-get update -qq 13 | - sudo apt-get install -y -qq docker-ce 14 | - curl -o- -L https://yarnpkg.com/install.sh | bash 15 | - export PATH="$HOME/.yarn/bin:$PATH" 16 | - npm i -g eslint eslint-config-aqua 17 | language: node_js 18 | node_js: 19 | - '8' 20 | install: npm test 21 | script: 22 | - bash ./deploy/deploy.sh 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # IDE 12 | .vscode 13 | 14 | # Compiled binary addons (http://nodejs.org/api/addons.html) 15 | build/Release 16 | 17 | # Dependency directory 18 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 19 | node_modules 20 | 21 | deploy/deploy_key 22 | deploy/deploy_key.pub 23 | 24 | docker-compose.yml 25 | 26 | assets/profile/backgrounds/** 27 | !assets/profile/backgrounds/default.png 28 | -------------------------------------------------------------------------------- /structures/currency/Store.js: -------------------------------------------------------------------------------- 1 | const { Collection } = require('discord.js'); 2 | 3 | const Item = require('../../models/Item'); 4 | const StoreItem = require('./StoreItem'); 5 | 6 | const storeItems = new Collection(); 7 | 8 | class Store { 9 | static registerItem(item) { 10 | storeItems.set(item.name, item); 11 | } 12 | 13 | static getItem(itemName) { 14 | return storeItems.get(itemName); 15 | } 16 | 17 | static getItems() { 18 | return storeItems; 19 | } 20 | } 21 | 22 | Item.findAll().then(items => { 23 | for (const item of items) Store.registerItem(new StoreItem(item.name, item.price)); 24 | }); 25 | 26 | module.exports = Store; 27 | -------------------------------------------------------------------------------- /structures/Redis.js: -------------------------------------------------------------------------------- 1 | const { promisifyAll } = require('tsubaki'); 2 | const redisClient = require('redis'); 3 | const winston = require('winston'); 4 | 5 | const { REDIS } = process.env; 6 | 7 | promisifyAll(redisClient.RedisClient.prototype); 8 | promisifyAll(redisClient.Multi.prototype); 9 | 10 | const redis = redisClient.createClient({ host: REDIS, port: 6379 }); 11 | 12 | class Redis { 13 | static get db() { 14 | return redis; 15 | } 16 | 17 | static start() { 18 | redis.on('error', error => winston.error(`[REDIS]: Encountered error: \n${error}`)) 19 | .on('reconnecting', () => winston.warn('[REDIS]: Reconnecting...')); 20 | } 21 | } 22 | 23 | module.exports = Redis; 24 | -------------------------------------------------------------------------------- /models/Tag.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | const Database = require('../structures/PostgreSQL'); 4 | 5 | const Tag = Database.db.define('tags', { 6 | userID: Sequelize.STRING, 7 | userName: Sequelize.STRING, 8 | guildID: Sequelize.STRING, 9 | guildName: Sequelize.STRING, 10 | name: Sequelize.STRING, 11 | content: Sequelize.STRING(1800), // eslint-disable-line new-cap 12 | type: { 13 | type: Sequelize.BOOLEAN, 14 | defaultValue: false 15 | }, 16 | example: { 17 | type: Sequelize.BOOLEAN, 18 | defaultValue: false 19 | }, 20 | exampleID: { 21 | type: Sequelize.STRING, 22 | defaultValue: '' 23 | }, 24 | uses: { 25 | type: Sequelize.INTEGER, 26 | defaultValue: 0 27 | } 28 | }); 29 | 30 | module.exports = Tag; 31 | -------------------------------------------------------------------------------- /util/Util.js: -------------------------------------------------------------------------------- 1 | class Util { 2 | constructor() { 3 | throw new Error(`The ${this.constructor.name} class may not be instantiated.`); 4 | } 5 | 6 | static cleanContent(msg, content) { 7 | return content.replace(/@everyone/g, '@\u200Beveryone') 8 | .replace(/@here/g, '@\u200Bhere') 9 | .replace(/<@&[0-9]+>/g, roles => { 10 | const replaceID = roles.replace(/<|&|>|@/g, ''); 11 | const role = msg.channel.guild.roles.get(replaceID); 12 | 13 | return `@${role.name}`; 14 | }) 15 | .replace(/<@!?[0-9]+>/g, user => { 16 | const replaceID = user.replace(/<|!|>|@/g, ''); 17 | const member = msg.channel.guild.members.get(replaceID); 18 | 19 | return `@${member.user.username}`; 20 | }); 21 | } 22 | } 23 | 24 | module.exports = Util; 25 | -------------------------------------------------------------------------------- /structures/games/RussianRoulette.js: -------------------------------------------------------------------------------- 1 | const games = new Map(); 2 | 3 | class RussianRoulette { 4 | constructor(guildID) { 5 | this.guildID = guildID; 6 | this.players = []; 7 | 8 | games.set(this.guildID, this); 9 | } 10 | 11 | join(user, donuts) { 12 | this.players.push({ 13 | user, 14 | donuts 15 | }); 16 | 17 | games.set(this.guildID, this); 18 | } 19 | 20 | hasPlayer(userID) { 21 | return !!this.players.find(player => player.user.id === userID); 22 | } 23 | 24 | awaitPlayers(time) { 25 | return new Promise(resolve => { 26 | setTimeout(() => { 27 | games.delete(this.guildID); 28 | return resolve(this.players || []); 29 | }, time); 30 | }); 31 | } 32 | 33 | static findGame(guildID) { 34 | return games.get(guildID) || null; 35 | } 36 | } 37 | 38 | module.exports = RussianRoulette; 39 | -------------------------------------------------------------------------------- /commands/tags/tag.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Tag = require('../../models/Tag'); 4 | 5 | module.exports = class TagCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'tag', 9 | group: 'tags', 10 | memberName: 'tag', 11 | description: 'Displays a tag.', 12 | guildOnly: true, 13 | throttling: { 14 | usages: 2, 15 | duration: 3 16 | }, 17 | 18 | args: [ 19 | { 20 | key: 'name', 21 | label: 'tagname', 22 | prompt: 'what tag would you like to see?\n', 23 | type: 'string', 24 | parse: str => str.toLowerCase() 25 | } 26 | ] 27 | }); 28 | } 29 | 30 | async run(msg, { name }) { 31 | const tag = await Tag.findOne({ where: { name, guildID: msg.guild.id } }); 32 | if (!tag) return null; 33 | tag.increment('uses'); 34 | 35 | return msg.say(tag.content); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /models/UserProfile.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | const Database = require('../structures/PostgreSQL'); 4 | 5 | const UserProfile = Database.db.define('userProfiles', { 6 | userID: Sequelize.STRING, 7 | inventory: { 8 | type: Sequelize.STRING, 9 | defaultValue: '[]' 10 | }, 11 | money: { 12 | type: Sequelize.BIGINT(), // eslint-disable-line new-cap 13 | defaultValue: 0 14 | }, 15 | balance: { 16 | type: Sequelize.BIGINT(), // eslint-disable-line new-cap 17 | defaultValue: 0 18 | }, 19 | networth: { 20 | type: Sequelize.BIGINT(), // eslint-disable-line new-cap 21 | defaultValue: 0 22 | }, 23 | experience: { 24 | type: Sequelize.BIGINT(), // eslint-disable-line new-cap 25 | defaultValue: 0 26 | }, 27 | personalMessage: { 28 | type: Sequelize.STRING, 29 | defaultValue: '' 30 | }, 31 | background: { 32 | type: Sequelize.STRING, 33 | defaultValue: 'default' 34 | } 35 | }); 36 | 37 | module.exports = UserProfile; 38 | -------------------------------------------------------------------------------- /commands/tags/source.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Tag = require('../../models/Tag'); 4 | 5 | module.exports = class TagSourceCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'tag-source', 9 | aliases: ['source-tag'], 10 | group: 'tags', 11 | memberName: 'source', 12 | description: 'Displays a tags source.', 13 | guildOnly: true, 14 | throttling: { 15 | usages: 2, 16 | duration: 3 17 | }, 18 | 19 | args: [ 20 | { 21 | key: 'name', 22 | label: 'tagname', 23 | prompt: 'what tag source would you like to see?\n', 24 | type: 'string', 25 | parse: str => str.toLowerCase() 26 | } 27 | ] 28 | }); 29 | } 30 | 31 | async run(msg, { name }) { 32 | const tag = await Tag.findOne({ where: { name, guildID: msg.guild.id } }); 33 | if (!tag) return msg.say(`A tag with the name **${name}** doesn't exist, ${msg.author}`); 34 | 35 | return msg.code('md', tag.content); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /commands/economy/unlock-all.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | 6 | module.exports = class UnlockAllCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'unlock-all', 10 | group: 'economy', 11 | memberName: 'unlock-all', 12 | description: `Enable xp and ${Currency.textSingular} earning on all channels in the server.`, 13 | guildOnly: true, 14 | throttling: { 15 | usages: 2, 16 | duration: 3 17 | } 18 | }); 19 | } 20 | 21 | hasPermission(msg) { 22 | return this.client.isOwner(msg.author) || msg.member.hasPermission('MANAGE_GUILD'); 23 | } 24 | 25 | run(msg) { 26 | this.client.provider.set(msg.guild.id, 'locks', []); 27 | return msg.reply(stripIndents` 28 | the lock on all channels has been lifted. 29 | You can now earn xp and ${Currency.textPlural} on the entire server again. 30 | `); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /structures/PostgreSQL.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const winston = require('winston'); 3 | 4 | const { DB } = process.env; 5 | const database = new Sequelize(DB, { logging: false }); 6 | 7 | class Database { 8 | static get db() { 9 | return database; 10 | } 11 | 12 | static start() { 13 | database.authenticate() 14 | .then(() => winston.info('[POSTGRES]: Connection to database has been established successfully.')) 15 | .then(() => winston.info('[POSTGRES]: Synchronizing database...')) 16 | .then(() => database.sync() 17 | .then(() => winston.info('[POSTGRES]: Done Synchronizing database!')) 18 | .catch(error => winston.error(`[POSTGRES]: Error synchronizing the database: \n${error}`)) 19 | ) 20 | .catch(error => { 21 | winston.error(`[POSTGRES]: Unable to connect to the database: \n${error}`); 22 | winston.error(`[POSTGRES]: Try reconnecting in 5 seconds...`); 23 | setTimeout(() => Database.start(), 5000); 24 | }); 25 | } 26 | } 27 | 28 | module.exports = Database; 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | LABEL maintainer "iCrawl " 4 | 5 | # Add package.json for Yarn 6 | WORKDIR /usr/src/Commando 7 | COPY package.json yarn.lock ./ 8 | 9 | # Install dependencies 10 | RUN apk add --update \ 11 | && apk add --no-cache ffmpeg opus pixman cairo pango giflib ca-certificates \ 12 | && apk add --no-cache --virtual .build-deps git curl pixman-dev cairo-dev pangomm-dev libjpeg-turbo-dev giflib-dev python g++ make \ 13 | \ 14 | # Install node.js dependencies 15 | && yarn install \ 16 | \ 17 | # Clean up build dependencies 18 | && apk del .build-deps 19 | 20 | # Add project source 21 | COPY . . 22 | 23 | ENV TOKEN= \ 24 | COMMAND_PREFIX= \ 25 | OWNERS= \ 26 | DB= \ 27 | REDIS= \ 28 | EXAMPLE_CHANNEL= \ 29 | WEATHER_API= \ 30 | GOOGLE_API= \ 31 | GOOGLE_CUSTOM_SEARCH= \ 32 | GOOGLE_CUSTOM_SEARCH_CX= \ 33 | SOUNDCLOUD_API= \ 34 | SHERLOCK_API= \ 35 | PAGINATED_ITEMS= \ 36 | DEFAULT_VOLUME= \ 37 | MAX_LENGTH= \ 38 | MAX_SONGS= \ 39 | PASSES= 40 | 41 | CMD ["node", "Commando.js"] 42 | -------------------------------------------------------------------------------- /structures/currency/Daily.js: -------------------------------------------------------------------------------- 1 | const Currency = require('./Currency'); 2 | const Redis = require('../Redis'); 3 | 4 | const DAY_DURATION = 24 * 60 * 60 * 1000; 5 | 6 | module.exports = class Daily { 7 | static get dailyPayout() { 8 | return 210; 9 | } 10 | 11 | static get dailyDonationPayout() { 12 | return 300; 13 | } 14 | 15 | static async received(userID) { 16 | const lastDaily = await Redis.db.getAsync(`daily${userID}`); 17 | if (!lastDaily) return false; 18 | 19 | return Date.now() - DAY_DURATION < lastDaily; 20 | } 21 | 22 | static async nextDaily(userID) { 23 | const lastDaily = await Redis.db.getAsync(`daily${userID}`); 24 | 25 | return DAY_DURATION - (Date.now() - lastDaily); 26 | } 27 | 28 | static async receive(userID, donationID) { 29 | if (donationID) Currency._changeBalance(donationID, Daily.dailyDonationPayout); 30 | else Currency._changeBalance(userID, Daily.dailyPayout); 31 | await Redis.db.setAsync(`daily${userID}`, Date.now()); 32 | await Redis.db.expireAsync(`daily${userID}`, DAY_DURATION / 1000); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 WeebDev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /commands/music/stop.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | module.exports = class StopMusicCommand extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'stop', 7 | aliases: ['kill', 'stfu'], 8 | group: 'music', 9 | memberName: 'stop', 10 | description: 'Stops the music and wipes the queue.', 11 | details: 'Only moderators may use this command.', 12 | guildOnly: true, 13 | throttling: { 14 | usages: 2, 15 | duration: 3 16 | } 17 | }); 18 | } 19 | 20 | hasPermission(msg) { 21 | return this.client.isOwner(msg.author) || msg.member.hasPermission('MANAGE_MESSAGES'); 22 | } 23 | 24 | run(msg) { 25 | const queue = this.queue.get(msg.guild.id); 26 | if (!queue) return msg.reply('there isn\'t any music playing right now.'); 27 | const song = queue.songs[0]; 28 | queue.songs = []; 29 | if (song.dispatcher) song.dispatcher.end(); 30 | 31 | return msg.reply('you\'ve just killed the party. Congrats. 👏'); 32 | } 33 | 34 | get queue() { 35 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 36 | 37 | return this._queue; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /types/emoji.js: -------------------------------------------------------------------------------- 1 | const { ArgumentType } = require('discord.js-commando'); 2 | const emojiRanges = [ 3 | '[\u0023-\u0039]\u20E3', 4 | '[\u2002-\u21AA]', 5 | '[\u231A-\u27bf]', 6 | '[\u2934-\u2b55]', 7 | '\u3030', '\u303D', 8 | '\u3297', '\u3299', 9 | '\uD83C[\udc04-\uDFFF]', 10 | '\uD83D[\uDC00-\uDE4F]' 11 | ]; 12 | const emojiRegex = new RegExp(emojiRanges.join('|'), 'g'); 13 | const regex = /<:([a-zA-Z0-9_]+):(\d+)>/; 14 | 15 | class EmojiArgumentType extends ArgumentType { 16 | constructor(client) { 17 | super(client, 'emoji'); 18 | } 19 | 20 | validate(value, msg) { 21 | if (value.match(regex)) { 22 | const emoji = msg.client.emojis.get(value.match(regex)[2]); 23 | if (emoji) return true; 24 | } else if (value.match(emojiRegex)) { 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | parse(value, msg) { // eslint-disable-line consistent-return 32 | if (value.match(regex)) { 33 | const emoji = msg.client.emojis.get(value.match(regex)[2]); 34 | if (emoji) return emoji; 35 | } else if (value.match(emojiRegex)) { 36 | return value.match(emojiRegex)[0]; 37 | } 38 | } 39 | } 40 | 41 | module.exports = EmojiArgumentType; 42 | -------------------------------------------------------------------------------- /commands/info/about.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | module.exports = class AboutCommand extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'about', 8 | group: 'info', 9 | memberName: 'about', 10 | description: 'Displays information about the command framework.', 11 | throttling: { 12 | usages: 2, 13 | duration: 3 14 | } 15 | }); 16 | } 17 | 18 | run(msg) { 19 | return msg.embed({ 20 | color: 3447003, 21 | description: stripIndents` 22 | __**discord.js Commando:**__ 23 | This is the WIP official command framework for discord.js. 24 | It makes full use of ES2017's \`async\`/\`await\`. 25 | 26 | [Framework GitHub](https://github.com/Gawdl3y/discord.js-commando) 27 | [Commando bot Github](https://github.com/WeebDev/Commando) 28 | 29 | __**Installation:**__ 30 | **Node 7.6.0 or newer is required.** 31 | \`npm i -S discord.js-commando\` 32 | 33 | [Documentation (WIP)](https://discord.js.org/#/docs/commando/) 34 | [Discord.js Documentation](https://discord.js.org/#/docs/main/) 35 | ` 36 | }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /deploy/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function test { 6 | npm test 7 | } 8 | 9 | if [[ "$TRAVIS_BRANCH" == revert-* ]]; then 10 | echo -e "\e[36m\e[1mBuild triggered for reversion branch \"${TRAVIS_BRANCH}\" - running nothing." 11 | exit 0 12 | fi 13 | 14 | if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 15 | echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running test." 16 | test 17 | exit 0 18 | fi 19 | 20 | if [ -n "$TRAVIS_TAG" ]; then 21 | echo -e "\e[36m\e[1mBuild triggered for tag \"${TRAVIS_TAG}\"." 22 | DOCKER_RELEASE="$TRAVIS_TAG" 23 | test 24 | docker login --username="$DOCKER_USERNAME" --password="$DOCKER_PASSWORD" 25 | docker build -t commando . 26 | docker tag commando:latest crawl/commando:"$DOCKER_RELEASE" 27 | docker push crawl/commando:"$DOCKER_RELEASE" 28 | else 29 | echo -e "\e[36m\e[1mBuild triggered for branch \"${TRAVIS_BRANCH}\"." 30 | DOCKER_RELEASE="latest" 31 | test 32 | docker login --username="$DOCKER_USERNAME" --password="$DOCKER_PASSWORD" 33 | docker build -t commando . 34 | docker tag commando:latest crawl/commando:"$DOCKER_RELEASE" 35 | docker push crawl/commando:"$DOCKER_RELEASE" 36 | fi 37 | -------------------------------------------------------------------------------- /commands/economy/bank.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | const moment = require('moment'); 4 | 5 | const Bank = require('../../structures/currency/Bank'); 6 | const Currency = require('../../structures/currency/Currency'); 7 | 8 | module.exports = class BankInfoCommand extends Command { 9 | constructor(client) { 10 | super(client, { 11 | name: 'bank', 12 | group: 'economy', 13 | memberName: 'bank', 14 | description: `Displays info about the bank.`, 15 | details: `Displays the balance and interest rate of the bank.`, 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | } 21 | }); 22 | } 23 | 24 | async run(msg) { 25 | const balance = await Currency.getBalance('bank'); 26 | const interestRate = await Bank.getInterestRate(); 27 | const nextUpdate = await Bank.nextUpdate(); 28 | 29 | return msg.reply(stripIndents` 30 | the bank currently has ${Currency.convert(balance)}. 31 | The current interest rate is ${(interestRate * 100).toFixed(3)}%. 32 | Interest will be applied in ${moment.duration(nextUpdate).format('hh [hours] mm [minutes]')}. 33 | `); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /commands/util/blacklist-user.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | module.exports = class BlacklistUserCommand extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'blacklist-user', 7 | aliases: ['blacklist'], 8 | group: 'util', 9 | memberName: 'blacklist-user', 10 | description: 'Prohibit a user from using commando', 11 | throttling: { 12 | usages: 2, 13 | duration: 3 14 | }, 15 | 16 | args: [ 17 | { 18 | key: 'user', 19 | prompt: 'whom do you want to blacklist?\n', 20 | type: 'user' 21 | } 22 | ] 23 | }); 24 | } 25 | 26 | hasPermission(msg) { 27 | return this.client.isOwner(msg.author); 28 | } 29 | 30 | run(msg, { user }) { 31 | if (this.client.isOwner(user.id)) return msg.reply('the bot owner can not be blacklisted.'); 32 | 33 | const blacklist = this.client.provider.get('global', 'userBlacklist', []); 34 | if (blacklist.includes(user.id)) return msg.reply('that user is already blacklisted.'); 35 | 36 | blacklist.push(user.id); 37 | this.client.provider.set('global', 'userBlacklist', blacklist); 38 | 39 | return msg.reply(`${user.tag} has been blacklisted from using ${this.client.user}.`); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /commands/util/whitelist-user.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | module.exports = class WhitelistUserCommand extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'whitelist-user', 7 | aliases: ['whitelist'], 8 | group: 'util', 9 | memberName: 'whitelist-user', 10 | description: 'Remove a user from the blacklist', 11 | throttling: { 12 | usages: 2, 13 | duration: 3 14 | }, 15 | 16 | args: [ 17 | { 18 | key: 'user', 19 | prompt: 'what user should get removed from the blacklist?\n', 20 | type: 'user' 21 | } 22 | ] 23 | }); 24 | } 25 | 26 | hasPermission(msg) { 27 | return this.client.isOwner(msg.author); 28 | } 29 | 30 | run(msg, { user }) { 31 | const blacklist = this.client.provider.get('global', 'userBlacklist', []); 32 | if (!blacklist.includes(user.id)) return msg.reply('that user is not blacklisted.'); 33 | 34 | const index = blacklist.indexOf(user.id); 35 | blacklist.splice(index, 1); 36 | 37 | if (blacklist.length === 0) this.client.provider.remove('global', 'userBlacklist'); 38 | else this.client.provider.set('global', 'userBlacklist', blacklist); 39 | 40 | return msg.reply(`${user.tag} has been removed from the blacklist.`); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /commands/item/info.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Store = require('../../structures/currency/Store'); 5 | 6 | module.exports = class StoreInfoCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'item-info', 10 | aliases: ['info-item'], 11 | group: 'item', 12 | memberName: 'info', 13 | description: 'Displays price of an item.', 14 | display: 'Displays price of an item.', 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | }, 19 | 20 | args: [ 21 | { 22 | key: 'item', 23 | prompt: 'which item would you like to know the price of?\n', 24 | type: 'string', 25 | parse: str => str.toLowerCase() 26 | } 27 | ] 28 | }); 29 | } 30 | 31 | run(msg, { item }) { 32 | const storeItem = Store.getItem(item); 33 | if (!storeItem) { 34 | return msg.reply(stripIndents` 35 | sorry, but that item doesn't exist. 36 | 37 | You can use ${msg.usage()} to get a list of the available items. 38 | `); 39 | } 40 | 41 | const storeItemName = storeItem.name.replace(/(\b\w)/gi, lc => lc.toUpperCase()); 42 | 43 | return msg.reply(`one ${storeItemName} costs ${storeItem.price} 🍩s`); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /commands/economy/lock-all.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | 6 | module.exports = class LockAllCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'lock-all', 10 | group: 'economy', 11 | memberName: 'lock-all', 12 | description: `Disable xp and ${Currency.textSingular} earning on all channels in the server.`, 13 | guildOnly: true, 14 | throttling: { 15 | usages: 2, 16 | duration: 3 17 | } 18 | }); 19 | } 20 | 21 | hasPermission(msg) { 22 | return this.client.isOwner(msg.author) || msg.member.hasPermission('MANAGE_GUILD'); 23 | } 24 | 25 | run(msg) { 26 | const channels = msg.guild.channels.filter(channel => channel.type === 'text'); 27 | const channelLocks = this.client.provider.get(msg.guild.id, 'locks', []); 28 | for (const channel of channels.values()) { 29 | if (channelLocks.includes(channel.id)) continue; 30 | channelLocks.push(channel.id); 31 | } 32 | 33 | this.client.provider.set(msg.guild.id, 'locks', channelLocks); 34 | 35 | return msg.reply(stripIndents` 36 | all channels on this server have been locked. You can no longer earn xp or ${Currency.textPlural} anywhere. 37 | `); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /commands/music/pause.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | module.exports = class PauseSongCommand extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'pause', 7 | aliases: ['shh', 'shhh', 'shhhh', 'shhhhh'], 8 | group: 'music', 9 | memberName: 'pause', 10 | description: 'Pauses the currently playing song.', 11 | guildOnly: true, 12 | throttling: { 13 | usages: 2, 14 | duration: 3 15 | } 16 | }); 17 | } 18 | 19 | hasPermission(msg) { 20 | return this.client.isOwner(msg.author) || msg.member.hasPermission('MANAGE_MESSAGES'); 21 | } 22 | 23 | run(msg) { 24 | const queue = this.queue.get(msg.guild.id); 25 | if (!queue) return msg.reply(`there isn't any music playing to pause, oh brilliant one.`); 26 | if (!queue.songs[0].dispatcher) return msg.reply('I can\'t pause a song that hasn\'t even begun playing yet.'); 27 | if (!queue.songs[0].playing) return msg.reply('pausing a song that is already paused is a bad move.'); 28 | queue.songs[0].dispatcher.pause(); 29 | queue.songs[0].playing = false; 30 | 31 | return msg.reply(`paused the music. Use \`${this.client.commandPrefix}resume\` to continue playing.`); 32 | } 33 | 34 | get queue() { 35 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 36 | 37 | return this._queue; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /commands/tags/list-all.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Tag = require('../../models/Tag'); 5 | 6 | module.exports = class TagListAllCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'tag-list-all', 10 | aliases: ['tags-all', 'list-all-tags', 'tagsall', 'listalltags'], 11 | group: 'tags', 12 | memberName: 'list-all', 13 | description: 'Lists all tags.', 14 | guildOnly: true, 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | } 19 | }); 20 | } 21 | 22 | hasPermission(msg) { 23 | return this.client.isOwner(msg.author) || msg.member.roles.exists('name', 'Server Staff'); 24 | } 25 | 26 | async run(msg) { 27 | const tags = await Tag.findAll({ where: { guildID: msg.guild.id } }); 28 | if (!tags) return msg.say(`${msg.guild.name} doesn't have any tags, ${msg.author}. Why not add one?`); 29 | 30 | const allTags = tags.filter(tag => !tag.type) 31 | .map(tag => tag.name) 32 | .sort() 33 | .join(', '); 34 | console.log(allTags); // eslint-disable-line 35 | /* eslint-disable newline-per-chained-call */ 36 | return msg.say(stripIndents`**❯ All tags:** 37 | ${allTags ? allTags : `${msg.guild.name} has no tags.`} 38 | `, { split: true, char: ', ' }); 39 | /* eslint-disable newline-per-chained-call */ 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /commands/games/roulette-info.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | 6 | module.exports = class RouletteInfo extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'roulette-info', 10 | group: 'games', 11 | memberName: 'roulette-info', 12 | description: 'Displays information about the roulette.', 13 | details: 'Displays information about the roulette.', 14 | guildOnly: true, 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | } 19 | }); 20 | } 21 | 22 | run(msg) { 23 | return msg.embed({ 24 | description: stripIndents` 25 | To start a game or place a bet use \`roulette <${Currency.textPlural}> \` 26 | 27 | \`<${Currency.textPlural}>\` for the amount of ${Currency.textPlural} to bet. 28 | Can only be 100, 200, 300, 400, 500, 1000, 2000 or 5000. 29 | 30 | \`\` is the space you want to bet on. Those should be written exactly as in the image below. 31 | 32 | **Payout multipliers:** 33 | *Single number* - 36x 34 | *Dozens* - 3x 35 | *Columns* - 3x 36 | *Halves* - 2x 37 | *Odd/Even* - 2x 38 | *Colors* - 2x 39 | 40 | **Examples:** 41 | \`roulette 300 2nd\` 42 | \`roulette 200 odd\` 43 | `, 44 | image: { url: 'https://a.safe.moe/lcfoa.png' } 45 | }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /commands/music/resume.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | module.exports = class ResumeSongCommand extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'resume', 7 | group: 'music', 8 | memberName: 'resume', 9 | description: 'Resumes the currently playing song.', 10 | details: 'Only moderators may use this command.', 11 | guildOnly: true, 12 | throttling: { 13 | usages: 2, 14 | duration: 3 15 | } 16 | }); 17 | } 18 | 19 | hasPermission(msg) { 20 | return this.client.isOwner(msg.author) || msg.member.hasPermission('MANAGE_MESSAGES'); 21 | } 22 | 23 | run(msg) { 24 | const queue = this.queue.get(msg.guild.id); 25 | if (!queue) return msg.reply(`there isn't any music playing to resume, oh brilliant one.`); 26 | if (!queue.songs[0].dispatcher) { 27 | return msg.reply('pretty sure a song that hasn\'t actually begun playing yet could be considered "resumed".'); 28 | } 29 | if (queue.songs[0].playing) return msg.reply('Resuming a song that isn\'t paused is a great move. Really fantastic.'); // eslint-disable-line max-len 30 | queue.songs[0].dispatcher.resume(); 31 | queue.songs[0].playing = true; 32 | 33 | return msg.reply('resumed the music. This party ain\'t over yet!'); 34 | } 35 | 36 | get queue() { 37 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 38 | 39 | return this._queue; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commando 2 | > Commando Discord bot built on discord.js-commando. 3 | 4 | ## Contributing 5 | 6 | 1. Fork it! 7 | 2. Create your feature branch: `git checkout -b my-new-feature` 8 | 3. Commit your changes: `git commit -am 'Add some feature'` 9 | 4. Push to the branch: `git push origin my-new-feature` 10 | 5. Submit a pull request :D 11 | 12 | 13 | ## Run it yourself 14 | 15 | ## Installation guide for Ubuntu 16.04.2 LTS 16 | 17 | #### Install Docker 18 | 19 | ```bash 20 | sudo apt-get update 21 | sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D 22 | sudo apt-add-repository 'deb https://apt.dockerproject.org/repo ubuntu-xenial main' 23 | sudo apt-get update 24 | sudo apt-get install -y docker-engine 25 | ``` 26 | 27 | #### Install docker-compose 28 | ```bash 29 | sudo pip install docker-compose 30 | ``` 31 | 32 | #### Get ready 33 | ```bash 34 | wget https://raw.githubusercontent.com/WeebDev/Commando/master/docker-compose.yml.example -O docker-compose.yml 35 | ``` 36 | 37 | ***Fill out all the needed ENV variables.*** 38 | 39 | #### Launch docker-compose 40 | 41 | ```bash 42 | docker-compose up -d 43 | ``` 44 | 45 | ## Author 46 | 47 | **Commando** © [WeebDev](https://github.com/WeebDev), Released under the [MIT](https://github.com/WeebDev/Commando/blob/master/LICENSE) License.
48 | Authored and maintained by WeebDev. 49 | 50 | > GitHub [@WeebDev](https://github.com/WeebDev) 51 | -------------------------------------------------------------------------------- /commands/economy/add.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Currency = require('../../structures/currency/Currency'); 4 | 5 | module.exports = class MoneyAddCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'add-money', 9 | aliases: [ 10 | 'money-add', 11 | 'add-donut', 12 | 'add-donuts', 13 | 'add-doughnut', 14 | 'add-doughnuts', 15 | 'donut-add', 16 | 'donuts-add', 17 | 'doughnut-add', 18 | 'doughnuts-add' 19 | ], 20 | group: 'economy', 21 | memberName: 'add', 22 | description: `Add ${Currency.textPlural} to a certain user.`, 23 | details: `Add amount of ${Currency.textPlural} to a certain user.`, 24 | guildOnly: true, 25 | throttling: { 26 | usages: 2, 27 | duration: 3 28 | }, 29 | 30 | args: [ 31 | { 32 | key: 'member', 33 | prompt: `what user would you like to give ${Currency.textPlural}?\n`, 34 | type: 'member' 35 | }, 36 | { 37 | key: 'donuts', 38 | label: 'amount of donuts to add', 39 | prompt: `how many ${Currency.textPlural} do you want to give that user?\n`, 40 | type: 'integer' 41 | } 42 | ] 43 | }); 44 | } 45 | 46 | hasPermission(msg) { 47 | return this.client.isOwner(msg.author); 48 | } 49 | 50 | run(msg, { member, donuts }) { 51 | Currency._changeBalance(member.id, donuts); 52 | return msg.reply(`successfully added ${Currency.convert(donuts)} to ${member.displayName}'s balance.`); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /commands/economy/lock.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | 6 | module.exports = class LockCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'lock', 10 | group: 'economy', 11 | memberName: 'lock', 12 | description: `Disable xp and ${Currency.textSingular} earning in a channel.`, 13 | guildOnly: true, 14 | throttling: { 15 | usages: 2, 16 | duration: 3 17 | }, 18 | 19 | args: [ 20 | { 21 | key: 'channel', 22 | prompt: 'what channel do you want to lock?\n', 23 | type: 'channel', 24 | default: '' 25 | } 26 | ] 27 | }); 28 | } 29 | 30 | hasPermission(msg) { 31 | return this.client.isOwner(msg.author) || msg.member.hasPermission('MANAGE_GUILD'); 32 | } 33 | 34 | run(msg, args) { 35 | const channel = args.channel || msg.channel; 36 | if (channel.type !== 'text') return msg.reply('you can only lock text channels.'); 37 | 38 | const channelLocks = this.client.provider.get(msg.guild.id, 'locks', []); 39 | if (channelLocks.includes(channel.id)) return msg.reply(`${channel} has already been locked.`); 40 | 41 | channelLocks.push(channel.id); 42 | this.client.provider.set(msg.guild.id, 'locks', channelLocks); 43 | 44 | return msg.reply(stripIndents` 45 | this channel has been locked. You can no longer earn xp or ${Currency.textPlural} in ${channel}. 46 | `); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /commands/tags/info.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const moment = require('moment'); 3 | 4 | const Tag = require('../../models/Tag'); 5 | 6 | module.exports = class TagWhoCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'tag-info', 10 | aliases: ['info-tag', 'tag-who', 'who-tag'], 11 | group: 'tags', 12 | memberName: 'info', 13 | description: 'Displays information about a tag.', 14 | guildOnly: true, 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | }, 19 | 20 | args: [ 21 | { 22 | key: 'name', 23 | label: 'tagname', 24 | prompt: 'what tag would you like to have information on?\n', 25 | type: 'string', 26 | parse: str => str.toLowerCase() 27 | } 28 | ] 29 | }); 30 | } 31 | 32 | async run(msg, { name }) { 33 | const tag = await Tag.findOne({ where: { name, guildID: msg.guild.id } }); 34 | if (!tag) return msg.say(`A tag with the name **${name}** doesn't exist, ${msg.author}`); 35 | 36 | return msg.embed({ 37 | color: 3447003, 38 | fields: [ 39 | { 40 | name: 'Username', 41 | value: `${tag.userName} (ID: ${tag.userID})` 42 | }, 43 | { 44 | name: 'Guild', 45 | value: `${tag.guildName}` 46 | }, 47 | { 48 | name: 'Created at', 49 | value: `${moment.utc(tag.createdAt).format('dddd, MMMM Do YYYY, HH:mm:ss ZZ')}` 50 | }, 51 | { 52 | name: 'Uses', 53 | value: `${tag.uses} ` 54 | } 55 | ] 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /commands/item/store.js: -------------------------------------------------------------------------------- 1 | const { Command, util } = require('discord.js-commando'); 2 | 3 | const { PAGINATED_ITEMS } = process.env; 4 | const Store = require('../../structures/currency/Store'); 5 | 6 | module.exports = class StoreInfoCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'store', 10 | group: 'item', 11 | memberName: 'store', 12 | description: 'Displays price of all items.', 13 | display: 'Displays price of all items.', 14 | throttling: { 15 | usages: 2, 16 | duration: 3 17 | }, 18 | 19 | args: [ 20 | { 21 | key: 'page', 22 | prompt: 'which page would you like to view?\n', 23 | type: 'integer', 24 | default: 1 25 | } 26 | ] 27 | }); 28 | } 29 | 30 | run(msg, { page }) { 31 | const storeItems = Store.getItems().array(); 32 | const paginated = util.paginate(storeItems, page, Math.floor(PAGINATED_ITEMS)); 33 | if (storeItems.length === 0) return msg.reply('can\'t show what we don\'t have, man.'); 34 | 35 | return msg.embed({ 36 | description: `__**Items:**__`, 37 | fields: [ 38 | { 39 | name: 'Item', 40 | value: paginated.items.map(item => item.name.replace(/(\b\w)/gi, lc => lc.toUpperCase())).join('\n'), 41 | inline: true 42 | }, 43 | { 44 | name: 'Price', 45 | value: paginated.items.map(item => item.price).join('\n'), 46 | inline: true 47 | } 48 | ], 49 | footer: { text: paginated.maxPage > 1 ? `Use ${msg.usage()} to view a specific page.` : '' } 50 | }); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /commands/music/default-volume.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const { DEFAULT_VOLUME } = process.env; 4 | 5 | module.exports = class DefaultVolumeCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'default-volume', 9 | group: 'music', 10 | memberName: 'default-volume', 11 | description: 'Shows or sets the default volume level.', 12 | format: '[level|"default"]', 13 | guildOnly: true, 14 | throttling: { 15 | usages: 2, 16 | duration: 3 17 | } 18 | }); 19 | } 20 | 21 | hasPermission(msg) { 22 | return this.client.isOwner(msg.author) || msg.member.hasPermission('ADMINISTRATOR'); 23 | } 24 | 25 | run(msg, args) { 26 | if (!args) { 27 | const defaultVolume = this.client.provider.get(msg.guild.id, 'defaultVolume', DEFAULT_VOLUME); 28 | return msg.reply(`the default volume level is ${defaultVolume}.`); 29 | } 30 | 31 | if (args.toLowerCase() === 'default') { 32 | this.client.provider.remove(msg.guild.id, 'defaultVolume'); 33 | return msg.reply(`set the default volume level to the bot's default (currently ${DEFAULT_VOLUME}).`); 34 | } 35 | 36 | const defaultVolume = parseInt(args); 37 | if (isNaN(defaultVolume) || defaultVolume <= 0 || defaultVolume > 10) { 38 | return msg.reply(`invalid number provided. It must be in the range of 0-10.`); 39 | } 40 | 41 | this.client.provider.set(msg.guild.id, 'defaultVolume', defaultVolume); 42 | 43 | return msg.reply(`set the default volume level to ${defaultVolume}.`); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /commands/music/save.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | module.exports = class SaveQueueCommand extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'save', 8 | aliases: ['save-songs', 'save-song-list'], 9 | group: 'music', 10 | memberName: 'save', 11 | description: 'Saves the queued songs.', 12 | guildOnly: true, 13 | throttling: { 14 | usages: 2, 15 | duration: 3 16 | } 17 | }); 18 | } 19 | 20 | run(msg) { 21 | const queue = this.queue.get(msg.guild.id); 22 | if (!queue) return msg.reply('there isn\'t any music playing right now. You should get on that.'); 23 | const song = queue.songs[0]; 24 | 25 | msg.reply('✔ Check your inbox!'); 26 | let embed = { 27 | color: 3447003, 28 | author: { 29 | name: `${msg.author.tag} (${msg.author.id})`, 30 | icon_url: msg.author.displayAvatarURL({ format: 'png' }) // eslint-disable-line camelcase 31 | }, 32 | description: stripIndents` 33 | **Currently playing:** 34 | ${song.url.match(/^https?:\/\/(api.soundcloud.com)\/(.*)$/) ? `${song}` : `[${song}](${`${song.url}`})`} 35 | ${song.url.match(/^https?:\/\/(api.soundcloud.com)\/(.*)$/) ? 'A SoundCloud song is currently playing.' : ''} 36 | `, 37 | image: { url: song.thumbnail } 38 | }; 39 | 40 | return msg.author.send('', { embed }); 41 | } 42 | 43 | get queue() { 44 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 45 | 46 | return this._queue; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /commands/economy/remove.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Currency = require('../../structures/currency/Currency'); 4 | 5 | module.exports = class MoneyRemoveCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'remove-money', 9 | aliases: [ 10 | 'money-remove', 11 | 'remove-donut', 12 | 'remove-donuts', 13 | 'remove-doughnut', 14 | 'remove-doughnuts', 15 | 'donut-remove', 16 | 'donuts-remove', 17 | 'doughnut-remove', 18 | 'doughnuts-remove' 19 | ], 20 | group: 'economy', 21 | memberName: 'remove', 22 | description: `Remove ${Currency.textPlural} from a certain user.`, 23 | details: `Remove amount of ${Currency.textPlural} from a certain user.`, 24 | guildOnly: true, 25 | throttling: { 26 | usages: 2, 27 | duration: 3 28 | }, 29 | 30 | args: [ 31 | { 32 | key: 'member', 33 | prompt: `what user would you like to remove ${Currency.textPlural} from?\n`, 34 | type: 'member' 35 | }, 36 | { 37 | key: 'donuts', 38 | label: 'amount of donuts to remove', 39 | prompt: `how many ${Currency.textPlural} do you want to remove from that user?\n`, 40 | type: 'integer' 41 | } 42 | ] 43 | }); 44 | } 45 | 46 | hasPermission(msg) { 47 | return this.client.isOwner(msg.author); 48 | } 49 | 50 | run(msg, { member, donuts }) { 51 | Currency._changeBalance(member.id, donuts); 52 | return msg.reply(`successfully removed ${Currency.convert(donuts)} from ${member.displayName}'s balance.`); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /commands/item/add.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Item = require('../../models/Item'); 4 | const Store = require('../../structures/currency/Store'); 5 | const StoreItem = require('../../structures/currency/StoreItem'); 6 | 7 | module.exports = class ItemAddCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'item-add', 11 | aliases: ['add-item'], 12 | group: 'item', 13 | memberName: 'add', 14 | description: 'Adds an item to the store.', 15 | details: 'Adds an item to the store.', 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'name', 24 | prompt: 'what should the new item be called?\n', 25 | type: 'string', 26 | parse: str => str.toLowerCase() 27 | }, 28 | { 29 | key: 'price', 30 | prompt: 'what should the new item cost?\n', 31 | type: 'integer', 32 | min: 1 33 | } 34 | ] 35 | }); 36 | } 37 | 38 | hasPermission(msg) { 39 | return this.client.isOwner(msg.author); 40 | } 41 | 42 | async run(msg, { name, price }) { 43 | const item = Store.getItem(name); 44 | if (item) return msg.reply('an item with that name already exists.'); 45 | 46 | const newItem = await Item.create({ 47 | name, 48 | price 49 | }); 50 | const newItemName = newItem.name.replace(/(\b\w)/gi, lc => lc.toUpperCase()); 51 | Store.registerItem(new StoreItem(newItem.name, newItem.price)); 52 | 53 | return msg.reply(`the item ${newItemName} has been successfully created!`); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /commands/economy/daily-random.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const moment = require('moment'); 3 | const { oneLine, stripIndents } = require('common-tags'); 4 | 5 | const Currency = require('../../structures/currency/Currency'); 6 | const Daily = require('../../structures/currency/Daily'); 7 | 8 | module.exports = class DailyRandomCommand extends Command { 9 | constructor(client) { 10 | super(client, { 11 | name: 'daily-random', 12 | aliases: ['daily-ran', 'daily-rng'], 13 | group: 'economy', 14 | memberName: 'daily-random', 15 | description: `Gift your daily ${Currency.textPlural} to a random online user.`, 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | } 21 | }); 22 | } 23 | 24 | async run(msg) { 25 | const guild = await msg.guild.members.fetch(); 26 | const member = guild.members.filter(mem => mem.presence.status === 'online' && !mem.user.bot).random(); 27 | const received = await Daily.received(msg.author.id); 28 | 29 | if (received) { 30 | const nextDaily = await Daily.nextDaily(msg.author.id); 31 | return msg.reply(stripIndents` 32 | you have already gifted your daily ${Currency.textPlural}. 33 | You can gift away your next daily in ${moment.duration(nextDaily).format('hh [hours] mm [minutes]')} 34 | `); 35 | } 36 | 37 | Daily.receive(msg.author.id, member.id); 38 | 39 | return msg.reply(oneLine` 40 | ${member.user.tag} (${member.id}) has successfully received your daily 41 | ${Currency.convert(Daily.dailyDonationPayout)}. 42 | `); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /commands/economy/unlock.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | 6 | module.exports = class UnlockCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'unlock', 10 | group: 'economy', 11 | memberName: 'unlock', 12 | description: `Enable xp and ${Currency.textSingular} earning in a channel.`, 13 | guildOnly: true, 14 | throttling: { 15 | usages: 2, 16 | duration: 3 17 | }, 18 | 19 | args: [ 20 | { 21 | key: 'channel', 22 | prompt: 'what channel do you want to unlock?\n', 23 | type: 'channel', 24 | default: '' 25 | } 26 | ] 27 | }); 28 | } 29 | 30 | hasPermission(msg) { 31 | return this.client.isOwner(msg.author) || msg.member.hasPermission('MANAGE_GUILD'); 32 | } 33 | 34 | run(msg, args) { 35 | const channel = args.channel || msg.channel; 36 | if (channel.type !== 'text') return msg.reply('you can only unlock text channels.'); 37 | 38 | const channelLocks = this.client.provider.get(msg.guild.id, 'locks', []); 39 | if (!channelLocks.includes(channel.id)) { 40 | return msg.reply('this channel is not locked.'); 41 | } 42 | 43 | const index = channelLocks.indexOf(channel.id); 44 | channelLocks.splice(index, 1); 45 | this.client.provider.set(msg.guild.id, 'locks', channelLocks); 46 | 47 | return msg.reply(stripIndents` 48 | the channel lock has been lifted. You can now earn xp and ${Currency.textPlural} in ${channel} again. 49 | `); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /commands/tags/list.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Tag = require('../../models/Tag'); 5 | 6 | module.exports = class TagListCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'tag-list', 10 | aliases: ['tags', 'list-tag', 'ahh'], 11 | group: 'tags', 12 | memberName: 'list', 13 | description: 'Lists all server tags.', 14 | guildOnly: true, 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | } 19 | }); 20 | } 21 | 22 | async run(msg) { 23 | const tags = await Tag.findAll({ where: { guildID: msg.guild.id } }); 24 | if (!tags) return msg.say(`${msg.guild.name} doesn't have any tags, ${msg.author}. Why not add one?`); 25 | 26 | const examples = tags.filter(tag => tag.type) 27 | .filter(tag => tag.example) 28 | .map(tag => tag.name) 29 | .sort() 30 | .join(', '); 31 | const usertags = tags.filter(tag => !tag.type) 32 | .filter(tag => tag.userID === msg.author.id) 33 | .map(tag => tag.name) 34 | .sort() 35 | .join(', '); 36 | /* eslint-disable newline-per-chained-call */ 37 | return msg.say(stripIndents`**❯ Tags:** 38 | ${tags.filter(tag => tag.type).filter(tag => !tag.example).map(tag => tag.name).sort().join(', ')} 39 | 40 | ${examples ? `**❯ Examples:** 41 | ${examples}` : `There are no examples.`} 42 | 43 | ${usertags ? `**❯ ${msg.member.displayName}'s tags:** 44 | ${usertags}` : `${msg.member.displayName} has no tags.`} 45 | `); 46 | /* eslint-disable newline-per-chained-call */ 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /commands/social/personal-message.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const UserProfile = require('../../models/UserProfile'); 4 | 5 | module.exports = class PersonalMessageCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'personal-message', 9 | aliases: ['set-personal-message', 'set-biography', 'biography', 'set-bio', 'bio'], 10 | group: 'social', 11 | memberName: 'personal-message', 12 | description: 'Set your personal message for your profile.', 13 | details: 'Set your personal message for your profile.', 14 | guildOnly: true, 15 | throttling: { 16 | usages: 1, 17 | duration: 30 18 | }, 19 | 20 | args: [ 21 | { 22 | key: 'personalMessage', 23 | prompt: 'what would you like to set as your personal message?\n', 24 | type: 'string', 25 | validate: value => { 26 | if (value.length > 130) { 27 | return ` 28 | your message was ${value.length} characters long. 29 | Please limit your personal message to 130 characters. 30 | `; 31 | } 32 | return true; 33 | } 34 | } 35 | ] 36 | }); 37 | } 38 | 39 | async run(msg, { personalMessage }) { 40 | const profile = await UserProfile.findOne({ where: { userID: msg.author.id } }); 41 | if (!profile) { 42 | await UserProfile.create({ 43 | userID: msg.author.id, 44 | personalMessage 45 | }); 46 | 47 | return msg.reply('your message has been updated!'); 48 | } 49 | 50 | profile.personalMessage = personalMessage; 51 | await profile.save(); 52 | 53 | return msg.reply('your message has been updated!'); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /docker-compose.yml.example: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | commando: 4 | image: "crawl/commando:latest" 5 | restart: always 6 | environment: 7 | - TOKEN=BOT_TOKEN 8 | - COMMAND_PREFIX=- 9 | - OWNERS=OWNER_IDS,CAN,BE,SEPERATED,BY,COMMAS 10 | - DB=postgres://POSTGRES_USER_DEFINED_IN_POSTGRES_ENV:POSTGRES_PW_DEFINED_IN_POSTGRES_ENV@postgres:5432/POSTGRES_DB_EITHER_USER_DEFINED_IN_POSTGRES_ENV_OR_POSTGRES_DB_DEFINED_IN_POSTGRES 11 | - REDIS=redis 12 | - EXAMPLE_CHANNEL=CHANNEL_TO_POST_EXAMPLES_IN 13 | - WEATHER_API=DARK_SKY_WEATHER_API_KEY 14 | - GOOGLE_API=GOOGLE_API_KEY_FOR_YOUTUBE_AND_GEOCODE 15 | - GOOGLE_CUSTOM_SEARCH=GOOGLE_API_KEY_FOR_CUSTOM_SEARCH 16 | - GOOGLE_CUSTOM_SEARCH_CX=GOOGLE_CX_FOR_CUSTOM_SEARCH 17 | - SOUNDCLOUD_API=SOUNDCLOUD_API_KEY 18 | - SHERLOCK_API=SHERLOCK_API_KEY 19 | - PAGINATED_ITEMS=HOW_MANY_ITEMS_PER_PAGE_VIA_PAGINATION 20 | - DEFAULT_VOLUME=DEFAULT_VOLUME_WHEN_JOINING_VOICE_CHANNEL 21 | - MAX_LENGTH=MAXIMUM_LENGTH_PER_SONG 22 | - MAX_SONGS=HOW_MANY_SONGS_A_USER_CAN_QUEUE_UP 23 | - PASSES=UPD_PACKET_PASS 24 | volumes: 25 | - LOCALHOST_VOLUME_PATH/backgrounds:/assets/profile/backgrounds 26 | depends_on: 27 | - postgres 28 | - redis 29 | 30 | postgres: 31 | image: "postgres:9-alpine" 32 | environment: 33 | - POSTGRES_USER=commando 34 | - POSTGRES_PASSWORD=POSTGRES_PW 35 | volumes: 36 | - LOCALHOST_VOLUME_PATH/postgresql/data:/var/lib/postgresql/data 37 | 38 | redis: 39 | image: "redis:3-alpine" 40 | volumes: 41 | - LOCALHOST_VOLUME_PATH/redis/data:/data 42 | -------------------------------------------------------------------------------- /commands/music/status.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Song = require('../../structures/Song'); 5 | 6 | module.exports = class MusicStatusCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'status', 10 | aliases: ['song', 'playing', 'current-song', 'now-playing'], 11 | group: 'music', 12 | memberName: 'status', 13 | description: 'Shows the current status of the music.', 14 | guildOnly: true, 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | } 19 | }); 20 | } 21 | 22 | run(msg) { 23 | const queue = this.queue.get(msg.guild.id); 24 | if (!queue) return msg.say('There isn\'t any music playing right now. You should get on that.'); 25 | const song = queue.songs[0]; 26 | const currentTime = song.dispatcher ? song.dispatcher.time / 1000 : 0; 27 | 28 | const embed = { 29 | color: 3447003, 30 | author: { 31 | name: `${song.username}`, 32 | icon_url: song.avatar // eslint-disable-line camelcase 33 | }, 34 | description: stripIndents` 35 | ${song.url.match(/^https?:\/\/(api.soundcloud.com)\/(.*)$/) ? `${song}` : `[${song}](${`${song.url}`})`} 36 | 37 | We are ${Song.timeString(currentTime)} into the song, and have ${song.timeLeft(currentTime)} left. 38 | ${!song.playing ? 'The music is paused.' : ''} 39 | `, 40 | image: { url: song.thumbnail } 41 | }; 42 | 43 | return msg.embed(embed); 44 | } 45 | 46 | get queue() { 47 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 48 | 49 | return this._queue; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /commands/util/remindme.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const moment = require('moment'); 3 | const sherlock = require('Sherlock'); 4 | const { stripIndents } = require('common-tags'); 5 | 6 | const Util = require('../../util/Util'); 7 | 8 | module.exports = class RemindMeCommand extends Command { 9 | constructor(client) { 10 | super(client, { 11 | name: 'remindme', 12 | aliases: ['remind'], 13 | group: 'util', 14 | memberName: 'remindme', 15 | description: 'Reminds you of something.', 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | }, 21 | 22 | args: [ 23 | { 24 | key: 'remind', 25 | label: 'reminder', 26 | prompt: 'what would you like me to remind you about?\n', 27 | type: 'string', 28 | validate: time => { 29 | const remindTime = sherlock.parse(time); 30 | if (!remindTime.startDate) return `please provide a valid starting time.`; 31 | 32 | return true; 33 | }, 34 | parse: time => sherlock.parse(time) 35 | } 36 | ] 37 | }); 38 | } 39 | 40 | async run(msg, { remind }) { 41 | const time = remind.startDate.getTime() - Date.now(); 42 | const preRemind = await msg.say(stripIndents` 43 | I will remind you '${Util.cleanContent(msg, remind.eventTitle)}' ${moment().add(time, 'ms').fromNow()}. 44 | `); 45 | const remindMessage = await new Promise(resolve => { 46 | setTimeout(() => resolve(msg.say(stripIndents` 47 | ${msg.author} you wanted me to remind you of: '${Util.cleanContent(msg, remind.eventTitle)}' 48 | `)), time); 49 | }); 50 | 51 | return [preRemind, remindMessage]; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /commands/util/stats.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const moment = require('moment'); 3 | require('moment-duration-format'); 4 | const { stripIndents } = require('common-tags'); 5 | 6 | const { version } = require('../../package'); 7 | 8 | module.exports = class StatsCommand extends Command { 9 | constructor(client) { 10 | super(client, { 11 | name: 'stats', 12 | aliases: ['statistics'], 13 | group: 'util', 14 | memberName: 'stats', 15 | description: 'Displays statistics about the bot.', 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | } 21 | }); 22 | } 23 | 24 | run(msg) { 25 | return msg.embed({ 26 | color: 3447003, 27 | description: '**Commando Statistics**', 28 | fields: [ 29 | { 30 | name: '❯ Uptime', 31 | value: moment.duration(this.client.uptime) 32 | .format('d[ days], h[ hours], m[ minutes, and ]s[ seconds]'), 33 | inline: true 34 | }, 35 | { 36 | name: '❯ Memory usage', 37 | value: `${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`, 38 | inline: true 39 | }, 40 | { 41 | name: '❯ General Stats', 42 | value: stripIndents` 43 | • Guilds: ${this.client.guilds.size} 44 | • Channels: ${this.client.channels.size} 45 | • Users: ${this.client.guilds.map(guild => guild.memberCount).reduce((a, b) => a + b)} 46 | `, 47 | inline: true 48 | }, 49 | { 50 | name: '❯ Version', 51 | value: `v${version}`, 52 | inline: true 53 | } 54 | ], 55 | thumbnail: { url: this.client.user.displayAvatarURL({ format: 'png' }) } 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /commands/music/max-songs.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { oneLine } = require('common-tags'); 3 | 4 | const { MAX_SONGS } = process.env; 5 | 6 | module.exports = class MaxSongsCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'max-songs', 10 | group: 'music', 11 | memberName: 'max-songs', 12 | description: 'Shows or sets the max songs per user.', 13 | format: '[amount|"default"]', 14 | details: oneLine` 15 | This is the maximum number of songs a user may have in the queue. 16 | The default is ${MAX_SONGS}. 17 | Only administrators may change this setting. 18 | `, 19 | guildOnly: true, 20 | throttling: { 21 | usages: 2, 22 | duration: 3 23 | } 24 | }); 25 | } 26 | 27 | hasPermission(msg) { 28 | return this.client.isOwner(msg.author) || msg.member.hasPermission('ADMINISTRATOR'); 29 | } 30 | 31 | run(msg, args) { 32 | if (!args) { 33 | const maxSongs = this.client.provider.get(msg.guild.id, 'maxSongs', MAX_SONGS); 34 | return msg.reply(`the maximum songs a user may have in the queue at one time is ${maxSongs}.`); 35 | } 36 | 37 | if (args.toLowerCase() === 'default') { 38 | this.client.provider.remove(msg.guild.id, 'maxSongs'); 39 | return msg.reply(`set the maximum songs to the default (currently ${MAX_SONGS}).`); 40 | } 41 | 42 | const maxSongs = parseInt(args); 43 | if (isNaN(maxSongs) || maxSongs <= 0) { 44 | return msg.reply(`invalid number provided.`); 45 | } 46 | 47 | this.client.provider.set(msg.guild.id, 'maxSongs', maxSongs); 48 | 49 | return msg.reply(`set the maximum songs to ${maxSongs}.`); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /commands/tags/add.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Tag = require('../../models/Tag'); 4 | const Util = require('../../util/Util'); 5 | 6 | module.exports = class TagAddCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'add-tag', 10 | aliases: ['tag-add'], 11 | group: 'tags', 12 | memberName: 'add', 13 | description: 'Adds a tag.', 14 | details: `Adds a tag, usable for everyone on the server. (Markdown can be used.)`, 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'name', 24 | label: 'tagname', 25 | prompt: 'what would you like to name it?\n', 26 | type: 'string' 27 | }, 28 | { 29 | key: 'content', 30 | label: 'tagcontent', 31 | prompt: 'what content would you like to add?\n', 32 | type: 'string', 33 | max: 1800 34 | } 35 | ] 36 | }); 37 | } 38 | 39 | async run(msg, args) { 40 | const name = Util.cleanContent(msg, args.name.toLowerCase()); 41 | const content = Util.cleanContent(msg, args.content); 42 | const tag = await Tag.findOne({ where: { name, guildID: msg.guild.id } }); 43 | if (tag) return msg.say(`A tag with the name **${name}** already exists, ${msg.author}`); 44 | 45 | await Tag.create({ 46 | userID: msg.author.id, 47 | userName: `${msg.author.tag}`, 48 | guildID: msg.guild.id, 49 | guildName: msg.guild.name, 50 | channelID: msg.channel.id, 51 | channelName: msg.channel.name, 52 | name, 53 | content 54 | }); 55 | 56 | return msg.say(`A tag with the name **${name}** has been added, ${msg.author}`); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /commands/economy/daily.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const moment = require('moment'); 3 | const { stripIndents } = require('common-tags'); 4 | 5 | const Currency = require('../../structures/currency/Currency'); 6 | const Daily = require('../../structures/currency/Daily'); 7 | 8 | module.exports = class DailyCommand extends Command { 9 | constructor(client) { 10 | super(client, { 11 | name: 'daily', 12 | group: 'economy', 13 | memberName: 'daily', 14 | description: `Receive or gift your daily ${Currency.textPlural}.`, 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'member', 24 | prompt: 'whom do you want to give your daily?\n', 25 | type: 'member', 26 | default: '' 27 | } 28 | ] 29 | }); 30 | } 31 | 32 | async run(msg, args) { 33 | const member = args.member || msg.member; 34 | const received = await Daily.received(msg.author.id); 35 | if (received) { 36 | const nextDaily = await Daily.nextDaily(msg.author.id); 37 | return msg.reply(stripIndents` 38 | you have already received your daily ${Currency.textPlural}. 39 | You can receive your next daily in ${moment.duration(nextDaily).format('hh [hours] mm [minutes]')} 40 | `); 41 | } 42 | 43 | if (member.id !== msg.author.id) { 44 | Daily.receive(msg.author.id, member.id); 45 | return msg.reply( 46 | `${member} has successfully received your daily ${Currency.convert(Daily.dailyDonationPayout)}.` 47 | ); 48 | } 49 | 50 | Daily.receive(msg.author.id); 51 | 52 | return msg.reply(`You have successfully received your daily ${Currency.convert(Daily.dailyPayout)}.`); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /commands/music/max-length.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { oneLine } = require('common-tags'); 3 | 4 | const { MAX_LENGTH } = process.env; 5 | 6 | module.exports = class MaxLengthCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'max-length', 10 | aliases: ['max-duration', 'max-song-length', 'max-song-duration'], 11 | group: 'music', 12 | memberName: 'max-length', 13 | description: 'Shows or sets the max song length.', 14 | format: '[minutes|"default"]', 15 | details: oneLine` 16 | This is the maximum length of a song that users may queue, in minutes. 17 | The default is ${MAX_LENGTH}. 18 | Only administrators may change this setting. 19 | `, 20 | guildOnly: true, 21 | throttling: { 22 | usages: 2, 23 | duration: 3 24 | } 25 | }); 26 | } 27 | 28 | hasPermission(msg) { 29 | return this.client.isOwner(msg.author) || msg.member.hasPermission('ADMINISTRATOR'); 30 | } 31 | 32 | run(msg, args) { 33 | if (!args) { 34 | const maxLength = this.client.provider.get(msg.guild.id, 'maxLength', MAX_LENGTH); 35 | return msg.reply(`the maximum length of a song is ${maxLength} minutes.`); 36 | } 37 | 38 | if (args.toLowerCase() === 'default') { 39 | this.client.provider.remove(msg.guild.id, 'maxLength'); 40 | return msg.reply(`set the maximum song length to the default (currently ${MAX_LENGTH} minutes).`); 41 | } 42 | 43 | const maxLength = parseInt(args); 44 | if (isNaN(maxLength) || maxLength <= 0) { 45 | return msg.reply(`invalid number provided.`); 46 | } 47 | 48 | this.client.provider.set(msg.guild.id, 'maxLength', maxLength); 49 | 50 | return msg.reply(`set the maximum song length to ${maxLength} minutes.`); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /commands/economy/deposit.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Bank = require('../../structures/currency/Bank'); 5 | const Currency = require('../../structures/currency/Currency'); 6 | 7 | module.exports = class DepositCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'deposit', 11 | group: 'economy', 12 | memberName: 'deposit', 13 | description: `Deposit ${Currency.textPlural} into the bank.`, 14 | details: `Deposit ${Currency.textPlural} into the bank.`, 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'donuts', 24 | label: 'amount of donuts to deposit', 25 | prompt: `how many ${Currency.textPlural} do you want to deposit?\n`, 26 | validate: donuts => /^(?:\d+|all|-all|-a)$/g.test(donuts), 27 | parse: async (donuts, msg) => { 28 | const balance = await Currency.getBalance(msg.author.id); 29 | 30 | if (['all', '-all', '-a'].includes(donuts)) return parseInt(balance); 31 | 32 | return parseInt(donuts); 33 | } 34 | } 35 | ] 36 | }); 37 | } 38 | 39 | async run(msg, { donuts }) { 40 | if (donuts <= 0) return msg.reply(`you can't deposit 0 or less ${Currency.convert(0)}.`); 41 | 42 | const userBalance = await Currency.getBalance(msg.author.id); 43 | if (userBalance < donuts) { 44 | return msg.reply(stripIndents` 45 | you don't have that many ${Currency.textPlural} to deposit! 46 | You currently have ${Currency.convert(userBalance)} on hand. 47 | `); 48 | } 49 | 50 | Bank.deposit(msg.author.id, donuts); 51 | 52 | return msg.reply(`successfully deposited ${Currency.convert(donuts)} to the bank!`); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /structures/Song.js: -------------------------------------------------------------------------------- 1 | const { Util } = require('discord.js'); 2 | const { oneLineTrim } = require('common-tags'); 3 | 4 | const { SOUNDCLOUD_API } = process.env; 5 | 6 | module.exports = class Song { 7 | constructor(video, member) { 8 | this.name = Util.escapeMarkdown(video.title); 9 | this.id = video.id; 10 | this.length = video.durationSeconds ? video.durationSeconds : video.duration / 1000; 11 | this.member = member; 12 | this.dispatcher = null; 13 | this.playing = false; 14 | } 15 | 16 | get url() { 17 | if (!isNaN(Number(this.id))) return `https://api.soundcloud.com/tracks/${this.id}/stream?client_id=${SOUNDCLOUD_API}`; // eslint-disable-line max-len 18 | else return `https://www.youtube.com/watch?v=${this.id}`; 19 | } 20 | 21 | get thumbnail() { 22 | const thumbnail = `https://img.youtube.com/vi/${this.id}/mqdefault.jpg`; 23 | 24 | return thumbnail; 25 | } 26 | 27 | get username() { 28 | const name = `${this.member.user.tag} (${this.member.user.id})`; 29 | 30 | return Util.escapeMarkdown(name); 31 | } 32 | 33 | get avatar() { 34 | const avatar = `${this.member.user.displayAvatarURL({ format: 'png' })}`; 35 | 36 | return avatar; 37 | } 38 | 39 | get lengthString() { 40 | return this.constructor.timeString(this.length); 41 | } 42 | 43 | timeLeft(currentTime) { 44 | return this.constructor.timeString(this.length - currentTime); 45 | } 46 | 47 | toString() { 48 | return `${this.name} (${this.lengthString})`; 49 | } 50 | 51 | static timeString(seconds, forceHours = false) { 52 | const hours = Math.floor(seconds / 3600); 53 | const minutes = Math.floor(seconds % 3600 / 60); 54 | 55 | return oneLineTrim` 56 | ${forceHours || hours >= 1 ? `${hours}:` : ''} 57 | ${hours >= 1 ? `0${minutes}`.slice(-2) : minutes}: 58 | ${`0${Math.floor(seconds % 60)}`.slice(-2)} 59 | `; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /commands/item/inventory.js: -------------------------------------------------------------------------------- 1 | const { Command, util } = require('discord.js-commando'); 2 | 3 | const Inventory = require('../../structures/currency/Inventory'); 4 | const { PAGINATED_ITEMS } = process.env; 5 | 6 | module.exports = class InventoryShowCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'inventory', 10 | aliases: ['inv'], 11 | group: 'item', 12 | memberName: 'inventory', 13 | description: 'Displays the items you have in your inventory', 14 | detail: 'Displays the items you have in your inventory', 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'page', 24 | prompt: 'what page would you like to view?\n', 25 | type: 'integer', 26 | default: 1 27 | } 28 | ] 29 | }); 30 | } 31 | 32 | async run(msg, { page }) { 33 | let items = []; 34 | const inventory = await Inventory.fetchInventory(msg.author.id); 35 | for (const item of Object.keys(inventory.content)) { 36 | items.push({ 37 | name: item, 38 | amount: inventory.content[item].amount 39 | }); 40 | } 41 | 42 | const paginated = util.paginate(items, page, Math.floor(PAGINATED_ITEMS)); 43 | if (items.length === 0) return msg.reply('can\'t show what you don\'t have, man.'); 44 | 45 | return msg.embed({ 46 | description: `__**${msg.author.tag}'s inventory:**__`, 47 | fields: [ 48 | { 49 | name: 'Item', 50 | value: paginated.items.map(item => item.name.replace(/(\b\w)/gi, lc => lc.toUpperCase())).join('\n'), 51 | inline: true 52 | }, 53 | { 54 | name: 'Amount', 55 | value: paginated.items.map(item => item.amount).join('\n'), 56 | inline: true 57 | } 58 | ], 59 | footer: { text: paginated.maxPage > 1 ? `Use ${msg.usage()} to view a specific page.` : '' } 60 | }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /commands/tags/add-server.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Tag = require('../../models/Tag'); 4 | const Util = require('../../util/Util'); 5 | 6 | module.exports = class ServerTagAddCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'add-server-tag', 10 | aliases: ['tag-add-server', 'add-servertag', 'servertag-add', 'servertag'], 11 | group: 'tags', 12 | memberName: 'add-server', 13 | description: 'Adds a server tag.', 14 | details: `Adds a server tag, usable for everyone on the server. (Markdown can be used.)`, 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'name', 24 | label: 'tagname', 25 | prompt: 'what would you like to name it?\n', 26 | type: 'string' 27 | }, 28 | { 29 | key: 'content', 30 | label: 'tagcontent', 31 | prompt: 'what content would you like to add?\n', 32 | type: 'string', 33 | max: 1800 34 | } 35 | ] 36 | }); 37 | } 38 | 39 | hasPermission(msg) { 40 | return this.client.isOwner(msg.author) || msg.member.roles.exists('name', 'Server Staff'); 41 | } 42 | 43 | async run(msg, args) { 44 | const name = Util.cleanContent(msg, args.name.toLowerCase()); 45 | const content = Util.cleanContent(msg, args.content); 46 | const tag = await Tag.findOne({ where: { name, guildID: msg.guild.id } }); 47 | if (tag) return msg.say(`A server tag with the name **${name}** already exists, ${msg.author}`); 48 | 49 | await Tag.create({ 50 | userID: msg.author.id, 51 | userName: `${msg.author.tag}`, 52 | guildID: msg.guild.id, 53 | guildName: msg.guild.name, 54 | channelID: msg.channel.id, 55 | channelName: msg.channel.name, 56 | name, 57 | content, 58 | type: true 59 | }); 60 | 61 | return msg.say(`A server tag with the name **${name}** has been added, ${msg.author}`); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /commands/economy/money.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { oneLine } = require('common-tags'); 3 | 4 | const Bank = require('../../structures/currency/Bank'); 5 | const Currency = require('../../structures/currency/Currency'); 6 | 7 | module.exports = class MoneyInfoCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'money', 11 | aliases: ['bal', 'balance', 'donut', 'donuts', 'doughnut', 'doughnuts'], 12 | group: 'economy', 13 | memberName: 'money', 14 | description: `Displays the ${Currency.textPlural} you have earned.`, 15 | details: `Displays the ${Currency.textPlural} you have earned.`, 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | }, 21 | 22 | args: [ 23 | { 24 | key: 'member', 25 | prompt: `whose ${Currency.textPlural} would you like to view?\n`, 26 | type: 'member', 27 | default: '' 28 | } 29 | ] 30 | }); 31 | } 32 | 33 | async run(msg, args) { 34 | const member = args.member || msg.author; 35 | const money = await Currency.getBalance(member.id); 36 | const balance = await Bank.getBalance(member.id) || 0; 37 | const networth = (money || 0) + balance; 38 | 39 | if (args.member) { 40 | if (money === null) return msg.reply(`${member.displayName} hasn't earned any ${Currency.textPlural} yet.`); 41 | return msg.reply(oneLine` 42 | ${member.displayName} has ${Currency.convert(money)} on hand and 43 | ${Currency.convert(balance)} in the bank. 44 | Their net worth is ${Currency.convert(networth)}. 45 | Good on them! 46 | `); 47 | } else { 48 | if (money === null) return msg.reply(`you haven't earned any ${Currency.textPlural} yet.`); 49 | return msg.reply(oneLine` 50 | you have ${Currency.convert(money)} on hand and 51 | ${Currency.convert(balance)} in the bank. 52 | Your net worth is ${Currency.convert(networth)}. 53 | Good on you! 54 | `); 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /commands/music/volume.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | module.exports = class ChangeVolumeCommand extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'volume', 7 | aliases: ['set-volume', 'set-vol', 'vol'], 8 | group: 'music', 9 | memberName: 'volume', 10 | description: 'Changes the volume.', 11 | format: '[level]', 12 | details: 'The volume level ranges from 0-10. You may specify "up" or "down" to modify the volume level by 2.', 13 | examples: ['volume', 'volume 7', 'volume up', 'volume down'], 14 | guildOnly: true, 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | } 19 | }); 20 | } 21 | 22 | run(msg, args) { 23 | const queue = this.queue.get(msg.guild.id); 24 | if (!queue) return msg.reply(`there isn't any music playing to change the volume of. Better queue some up!`); 25 | if (!args) return msg.reply(`the dial is currently set to ${queue.volume}.`); 26 | if (!queue.voiceChannel.members.has(msg.author.id)) { 27 | return msg.reply(`you're not in the voice channel. You better not be trying to mess with their mojo, man.`); 28 | } 29 | 30 | let volume = parseInt(args); 31 | if (isNaN(volume)) { 32 | volume = args.toLowerCase(); 33 | if (volume === 'up' || volume === '+') volume = queue.volume + 2; 34 | else if (volume === 'down' || volume === '-') volume = queue.volume - 2; 35 | else return msg.reply(`invalid volume level. The dial goes from 0-10, baby.`); 36 | if (volume === 11) volume = 10; 37 | } 38 | 39 | volume = Math.min(Math.max(volume, 0), volume === 11 ? 11 : 10); 40 | queue.volume = volume; 41 | if (queue.songs[0].dispatcher) queue.songs[0].dispatcher.setVolumeLogarithmic(queue.volume / 5); 42 | 43 | return msg.reply(`${volume === 11 ? 'this one goes to 11!' : `set the dial to ${volume}.`}`); 44 | } 45 | 46 | get queue() { 47 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 48 | 49 | return this._queue; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /commands/tags/delete.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const { EXAMPLE_CHANNEL } = process.env; 4 | const Tag = require('../../models/Tag'); 5 | 6 | module.exports = class TagDeleteCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'delete-tag', 10 | aliases: [ 11 | 'tag-delete', 12 | 'tag-del', 13 | 'tag-example-delete', 14 | 'tag-example-del', 15 | 'tag-server-del', 16 | 'tag-servertag-del', 17 | 'delete-example', 18 | 'delete-servertag', 19 | 'delete-server', 20 | 'example-delete', 21 | 'servertag-delete', 22 | 'server-delete', 23 | 'del-tag', 24 | 'del-example', 25 | 'del-servertag', 26 | 'del-server', 27 | 'servertag-del', 28 | 'server-del', 29 | 'example-del' 30 | ], 31 | group: 'tags', 32 | memberName: 'delete', 33 | description: 'Deletes a tag.', 34 | guildOnly: true, 35 | throttling: { 36 | usages: 2, 37 | duration: 3 38 | }, 39 | 40 | args: [ 41 | { 42 | key: 'name', 43 | label: 'tagname', 44 | prompt: 'what tag would you like to delete?\n', 45 | type: 'string', 46 | parse: str => str.toLowerCase() 47 | } 48 | ] 49 | }); 50 | } 51 | 52 | async run(msg, { name }) { 53 | const staffRole = this.client.isOwner(msg.author) || await msg.member.roles.exists('name', 'Server Staff'); 54 | const tag = await Tag.findOne({ where: { name, guildID: msg.guild.id } }); 55 | if (!tag) return msg.say(`A tag with the name **${name}** doesn't exist, ${msg.author}`); 56 | if (tag.userID !== msg.author.id && !staffRole) return msg.say(`You can only delete your own tags, ${msg.author}`); 57 | 58 | Tag.destroy({ where: { name, guildID: msg.guild.id } }); 59 | if (tag.example) { 60 | const messageToDelete = await msg.guild.channels.get(EXAMPLE_CHANNEL).messages.fetch(tag.exampleID); 61 | messageToDelete.delete(); 62 | } 63 | 64 | return msg.say(`The tag **${name}** has been deleted, ${msg.author}`); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /commands/item/give.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Inventory = require('../../structures/currency/Inventory'); 4 | const ItemGroup = require('../../structures/currency/ItemGroup'); 5 | 6 | module.exports = class ItemGiveCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'give', 10 | aliases: ['give-item', 'give-itmes', 'item-give', 'items-give'], 11 | group: 'item', 12 | memberName: 'give', 13 | description: `Give your items to another user.`, 14 | guildOnly: true, 15 | throttling: { 16 | usages: 2, 17 | duration: 3 18 | }, 19 | 20 | args: [ 21 | { 22 | key: 'member', 23 | prompt: 'what user would you like to give your item(s)?\n', 24 | type: 'member' 25 | }, 26 | { 27 | key: 'item', 28 | prompt: 'what item would you like to give?\n', 29 | type: 'string' 30 | }, 31 | { 32 | key: 'amount', 33 | label: 'amount of items to give', 34 | prompt: 'how many do you want to give?\n', 35 | type: 'integer', 36 | min: 1 37 | } 38 | ] 39 | }); 40 | } 41 | 42 | async run(msg, { member, amount, item }) { 43 | const item2 = ItemGroup.convert(item, amount); 44 | const inventory = await Inventory.fetchInventory(msg.author.id); 45 | const itemBalance = inventory.content[item2] ? inventory.content[item2].amount : 0; 46 | 47 | if (member.id === msg.author.id) return msg.reply("giving items to yourself won't change anything."); 48 | if (member.user.bot) return msg.reply("don't give your items to bots: they're bots, man."); 49 | if (amount <= 0) return msg.reply("you can't give 0 or less items."); 50 | if (amount > itemBalance) return msg.reply(`you have ${itemBalance} ${item2}(s).`); 51 | 52 | const itemGroup = new ItemGroup(item2, amount); 53 | const receiveInv = await Inventory.fetchInventory(member.id); 54 | inventory.removeItems(itemGroup); 55 | receiveInv.addItems(itemGroup); 56 | 57 | return msg.reply(`${member.displayName} successfully received your item(s)!`); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /commands/info/server-info.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const moment = require('moment'); 3 | const { stripIndents } = require('common-tags'); 4 | 5 | const humanLevels = { 6 | 0: 'None', 7 | 1: 'Low', 8 | 2: 'Medium', 9 | 3: '(╯°□°)╯︵ ┻━┻', 10 | 4: '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻' 11 | }; 12 | 13 | module.exports = class ServerInfoCommand extends Command { 14 | constructor(client) { 15 | super(client, { 16 | name: 'server-info', 17 | aliases: ['server'], 18 | group: 'info', 19 | memberName: 'server', 20 | description: 'Get info on the server.', 21 | details: `Get detailed information on the server.`, 22 | guildOnly: true, 23 | throttling: { 24 | usages: 2, 25 | duration: 3 26 | } 27 | }); 28 | } 29 | 30 | run(msg) { 31 | return msg.embed({ 32 | color: 3447003, 33 | description: `Info on **${msg.guild.name}** (ID: ${msg.guild.id})`, 34 | fields: [ 35 | { 36 | name: '❯ Channels', 37 | /* eslint-disable max-len */ 38 | value: stripIndents` 39 | • ${msg.guild.channels.filter(ch => ch.type === 'text').size} Text, ${msg.guild.channels.filter(ch => ch.type === 'voice').size} Voice 40 | • Default: ${msg.guild.defaultChannel} 41 | • AFK: ${msg.guild.afkChannelID ? `<#${msg.guild.afkChannelID}> after ${msg.guild.afkTimeout / 60}min` : 'None.'} 42 | `, 43 | /* eslint-enable max-len */ 44 | inline: true 45 | }, 46 | { 47 | name: '❯ Member', 48 | value: stripIndents` 49 | • ${msg.guild.memberCount} members 50 | • Owner: ${msg.guild.owner.user.tag} 51 | (ID: ${msg.guild.ownerID}) 52 | `, 53 | inline: true 54 | }, 55 | { 56 | name: '❯ Other', 57 | value: stripIndents` 58 | • Roles: ${msg.guild.roles.size} 59 | • Region: ${msg.guild.region} 60 | • Created at: ${moment.utc(msg.guild.createdAt).format('dddd, MMMM Do YYYY, HH:mm:ss ZZ')} 61 | • Verification Level: ${humanLevels[msg.guild.verificationLevel]} 62 | ` 63 | } 64 | ], 65 | thumbnail: { url: msg.guild.iconURL } 66 | }); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /commands/economy/withdraw.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Bank = require('../../structures/currency/Bank'); 5 | const Currency = require('../../structures/currency/Currency'); 6 | 7 | module.exports = class WidthdrawCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'withdraw', 11 | group: 'economy', 12 | memberName: 'withdraw', 13 | description: `Withdraw ${Currency.textPlural} from the bank.`, 14 | details: `Withdraw ${Currency.textPlural} from the bank.`, 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'donuts', 24 | label: 'amount of donuts to withdraw', 25 | prompt: `how many ${Currency.textPlural} do you want to withdraw?\n`, 26 | validate: donuts => /^(?:\d+|all|-all|-a)$/g.test(donuts), 27 | parse: async (donuts, msg) => { 28 | const balance = await Bank.getBalance(msg.author.id); 29 | 30 | if (['all', '-all', '-a'].includes(donuts)) return parseInt(balance); 31 | 32 | return parseInt(donuts); 33 | } 34 | } 35 | ] 36 | }); 37 | } 38 | 39 | async run(msg, { donuts }) { 40 | if (donuts <= 0) return msg.reply(`you can't widthdraw 0 or less ${Currency.convert(0)}.`); 41 | 42 | const userBalance = await Bank.getBalance(msg.author.id); 43 | if (userBalance < donuts) { 44 | return msg.reply(stripIndents` 45 | you do not have that many ${Currency.textPlural} in your balance! 46 | Your current balance is ${Currency.convert(userBalance)}. 47 | `); 48 | } 49 | 50 | const bankBalance = await Currency.getBalance('bank'); 51 | if (bankBalance < donuts) { 52 | return msg.reply(stripIndents` 53 | sorry, but the bank doesn't have enough ${Currency.textPlural} for you to withdraw! 54 | Please try again later. 55 | `); 56 | } 57 | 58 | Bank.withdraw(msg.author.id, donuts); 59 | 60 | return msg.reply(`successfully withdrew ${Currency.convert(donuts)} from the bank!`); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /structures/currency/Experience.js: -------------------------------------------------------------------------------- 1 | const UserProfile = require('../../models/UserProfile'); 2 | const Redis = require('../Redis'); 3 | 4 | setInterval(() => Experience.saveExperience(), 30 * 60 * 1000); 5 | 6 | class Experience { 7 | static addExperience(userID, earned) { 8 | return Redis.db.hgetAsync('experience', userID).then(async balance => { 9 | const bal = parseInt(balance) || 0; 10 | await Redis.db.hsetAsync('experience', userID, earned + parseInt(bal)); 11 | }); 12 | } 13 | 14 | static removeExperience(userID, earned) { 15 | Experience.addExperience(userID, -earned); 16 | } 17 | 18 | static async getTotalExperience(userID) { 19 | const experience = await Redis.db.hgetAsync('experience', userID) || 0; 20 | 21 | return experience; 22 | } 23 | 24 | static async getCurrentExperience(userID) { 25 | const totalXP = await Experience.getTotalExperience(userID); 26 | const level = await Experience.getLevel(userID); 27 | const { lowerBound } = Experience.getLevelBounds(level); 28 | 29 | return totalXP - lowerBound; 30 | } 31 | 32 | static getLevelBounds(level) { 33 | const upperBound = Math.ceil((level / 0.177) ** 2); 34 | const lowerBound = Math.ceil(((level - 1) / 0.177) ** 2); 35 | 36 | return { 37 | upperBound, 38 | lowerBound 39 | }; 40 | } 41 | 42 | static async getLevel(userID) { 43 | const experience = await Experience.getTotalExperience(userID); 44 | 45 | return Math.floor(0.177 * Math.sqrt(experience)) + 1; 46 | } 47 | 48 | static async saveExperience() { 49 | const experiences = await Redis.db.hgetallAsync('experience'); 50 | const ids = Object.keys(experiences || {}); 51 | 52 | /* eslint-disable no-await-in-loop */ 53 | for (const id of ids) { 54 | const user = await UserProfile.findOne({ where: { userID: id } }); 55 | if (!user) { 56 | await UserProfile.create({ 57 | userID: id, 58 | experience: experiences[id] 59 | }); 60 | } else { 61 | user.update({ experience: experiences[id] }); 62 | } 63 | } 64 | /* eslint-enable no-await-in-loop */ 65 | } 66 | } 67 | 68 | module.exports = Experience; 69 | -------------------------------------------------------------------------------- /commands/social/blame.js: -------------------------------------------------------------------------------- 1 | const Canvas = require('canvas'); 2 | const { Command } = require('discord.js-commando'); 3 | const path = require('path'); 4 | 5 | module.exports = class BlameCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'blame', 9 | group: 'social', 10 | memberName: 'blame', 11 | description: 'Put the blame on someone.', 12 | guildOnly: true, 13 | throttling: { 14 | usages: 1, 15 | duration: 10 16 | }, 17 | 18 | args: [ 19 | { 20 | key: 'member', 21 | prompt: 'whom would you like to blame?\n', 22 | type: 'member', 23 | default: '' 24 | } 25 | ] 26 | }); 27 | } 28 | 29 | run(msg, args) { 30 | const member = args.member.displayName || 'Crawl'; 31 | 32 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'profile', 'fonts', 'Roboto.ttf'), { family: 'Roboto' }); // eslint-disable-line max-len 33 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'profile', 'fonts', 'NotoEmoji-Regular.ttf'), { family: 'Roboto' }); // eslint-disable-line max-len 34 | 35 | const canvas = new Canvas(); 36 | const ctx = canvas.getContext('2d'); 37 | const { width, height } = this._textSizes(ctx, member); 38 | 39 | canvas.width = width < 130 ? 130 : width; 40 | canvas.height = height; 41 | 42 | const generate = () => { 43 | ctx.font = '700 32px Roboto'; 44 | ctx.fillStyle = '#B93F2C'; 45 | ctx.textAlign = 'center'; 46 | ctx.fillText('Blame', canvas.width / 2, 35); 47 | 48 | ctx.fillStyle = '#F01111'; 49 | ctx.fillText(member, canvas.width / 2, 70); 50 | }; 51 | generate(); 52 | 53 | return msg.channel.send({ files: [{ attachment: canvas.toBuffer(), name: 'blame.png' }] }); 54 | } 55 | 56 | _textSizes(ctx, text) { 57 | ctx.font = '700 32px Arial'; 58 | const dimensions = ctx.measureText(text); 59 | const sizes = { 60 | width: dimensions.width + 20, 61 | height: dimensions.emHeightAscent + 54 62 | }; 63 | if (dimensions.actualBoundingBoxDescent) sizes.height += dimensions.actualBoundingBoxDescent - 3; 64 | 65 | return sizes; 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /commands/info/user-info.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const moment = require('moment'); 3 | const { stripIndents } = require('common-tags'); 4 | 5 | const username = require('../../models/UserName'); 6 | 7 | module.exports = class UserInfoCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'user-info', 11 | aliases: ['user'], 12 | group: 'info', 13 | memberName: 'user', 14 | description: 'Get info on a user.', 15 | details: `Get detailed information on the specified user.`, 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | }, 21 | 22 | args: [ 23 | { 24 | key: 'member', 25 | prompt: 'what user would you like to have information on?\n', 26 | type: 'member', 27 | default: '' 28 | } 29 | ] 30 | }); 31 | } 32 | 33 | async run(msg, args) { 34 | const member = args.member || msg.member; 35 | const { user } = member; 36 | const usernames = await username.findAll({ where: { userID: user.id } }); 37 | return msg.embed({ 38 | color: 3447003, 39 | fields: [ 40 | { 41 | name: '❯ Member Details', 42 | value: stripIndents` 43 | ${member.nickname !== null ? ` • Nickname: ${member.nickname}` : '• No nickname'} 44 | • Roles: ${member.roles.map(roles => `\`${roles.name}\``).join(' ')} 45 | • Joined at: ${moment.utc(member.joinedAt).format('dddd, MMMM Do YYYY, HH:mm:ss ZZ')} 46 | ` 47 | }, 48 | { 49 | name: '❯ User Details', 50 | /* eslint-disable max-len */ 51 | value: stripIndents` 52 | • Created at: ${moment.utc(user.createdAt).format('dddd, MMMM Do YYYY, HH:mm:ss ZZ')}${user.bot ? '\n• Is a bot account' : ''} 53 | • Aliases: ${usernames.length ? usernames.map(uName => uName.username).join(', ') : user.username} 54 | • Status: ${user.presence.status} 55 | • Game: ${user.presence.game ? user.presence.game.name : 'None'} 56 | ` 57 | /* eslint-enable max-len */ 58 | } 59 | ], 60 | thumbnail: { url: user.displayAvatarURL({ format: 'png' }) } 61 | }); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /commands/tags/add-example.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const { EXAMPLE_CHANNEL } = process.env; 4 | const Tag = require('../../models/Tag'); 5 | const Util = require('../../util/Util'); 6 | 7 | module.exports = class ExampleAddCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'add-example', 11 | aliases: ['example-add', 'tag-add-example', 'add-example-tag'], 12 | group: 'tags', 13 | memberName: 'add-example', 14 | description: 'Adds an example.', 15 | details: `Adds an example and posts it into the #examples channel. (Markdown can be used.)`, 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | }, 21 | 22 | args: [ 23 | { 24 | key: 'name', 25 | label: 'examplename', 26 | prompt: 'what would you like to name it?\n', 27 | type: 'string' 28 | }, 29 | { 30 | key: 'content', 31 | label: 'examplecontent', 32 | prompt: 'what content would you like to add?\n', 33 | type: 'string', 34 | max: 1800 35 | } 36 | ] 37 | }); 38 | } 39 | 40 | hasPermission(msg) { 41 | return this.client.isOwner(msg.author) || msg.member.roles.exists('name', 'Server Staff'); 42 | } 43 | 44 | async run(msg, args) { 45 | const name = Util.cleanContent(msg, args.name.toLowerCase()); 46 | const content = Util.cleanContent(msg, args.content); 47 | const tag = await Tag.findOne({ where: { name, guildID: msg.guild.id } }); 48 | if (tag) return msg.say(`An example with the name **${name}** already exists, ${msg.author}`); 49 | 50 | await Tag.create({ 51 | userID: msg.author.id, 52 | userName: `${msg.author.tag}`, 53 | guildID: msg.guild.id, 54 | guildName: msg.guild.name, 55 | channelID: msg.channel.id, 56 | channelName: msg.channel.name, 57 | name, 58 | content, 59 | type: true, 60 | example: true 61 | }); 62 | 63 | const message = await msg.guild.channels.get(EXAMPLE_CHANNEL).send(content); 64 | Tag.update({ exampleID: message.id }, { where: { name, guildID: msg.guild.id } }); 65 | 66 | return msg.say(`An example with the name **${name}** has been added, ${msg.author}`); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /commands/social/please.js: -------------------------------------------------------------------------------- 1 | const Canvas = require('canvas'); 2 | const { Command } = require('discord.js-commando'); 3 | const path = require('path'); 4 | 5 | module.exports = class PleaseCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'please', 9 | aliases: ['pls'], 10 | group: 'social', 11 | memberName: 'please', 12 | description: 'Make someone else plead?..', 13 | guildOnly: true, 14 | throttling: { 15 | usages: 1, 16 | duration: 10 17 | }, 18 | 19 | args: [ 20 | { 21 | key: 'member', 22 | prompt: 'whom would you like to make plead?\n', 23 | type: 'member', 24 | default: '' 25 | } 26 | ] 27 | }); 28 | } 29 | 30 | run(msg, args) { 31 | const member = args.member.displayName || 'Grey'; 32 | 33 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'profile', 'fonts', 'Roboto.ttf'), { family: 'Roboto' }); // eslint-disable-line max-len 34 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'profile', 'fonts', 'NotoEmoji-Regular.ttf'), { family: 'Roboto' }); // eslint-disable-line max-len 35 | 36 | const canvas = new Canvas(); 37 | const ctx = canvas.getContext('2d'); 38 | const { width, height } = this.textSizes(ctx, member); 39 | 40 | canvas.width = width < 130 ? 130 : width; 41 | canvas.height = height; 42 | 43 | const generate = () => { 44 | ctx.font = '700 32px Roboto'; 45 | ctx.fillStyle = '#B93F2C'; 46 | ctx.textAlign = 'center'; 47 | ctx.fillText(member, canvas.width / 2, 35); 48 | 49 | ctx.fillStyle = '#F01111'; 50 | ctx.fillText('Pls', canvas.width / 2, 70); 51 | }; 52 | generate(); 53 | 54 | return msg.channel.send({ files: [{ attachment: canvas.toBuffer(), name: 'please.png' }] }); 55 | } 56 | 57 | textSizes(ctx, text) { 58 | ctx.font = '700 32px Arial'; 59 | const dimensions = ctx.measureText(text); 60 | const sizes = { 61 | width: dimensions.width + 20, 62 | height: dimensions.emHeightAscent + 54 63 | }; 64 | if (dimensions.actualBoundingBoxDescent) sizes.height += dimensions.actualBoundingBoxDescent - 3; 65 | 66 | return sizes; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /structures/currency/Inventory.js: -------------------------------------------------------------------------------- 1 | const UserProfile = require('../../models/UserProfile'); 2 | const ItemGroup = require('./ItemGroup'); 3 | const Redis = require('../Redis'); 4 | 5 | setInterval(async () => { 6 | const inventories = await Redis.db.hgetallAsync('inventory'); 7 | const ids = Object.keys(inventories || {}); 8 | 9 | /* eslint-disable no-await-in-loop */ 10 | for (const id of ids) { 11 | const user = UserProfile.findOne({ where: { userID: id } }); 12 | if (!user) { 13 | await UserProfile.create({ 14 | userID: id, 15 | inventory: JSON.stringify(inventories[id]) 16 | }); 17 | } else { 18 | user.update({ inventory: JSON.stringify(inventories[id]) }); 19 | } 20 | } 21 | /* eslint-enable no-await-in-loop */ 22 | }, 30 * 60 * 1000); 23 | 24 | class Inventory { 25 | constructor(user, content) { 26 | this.user = user; 27 | this.content = content || {}; 28 | } 29 | 30 | addItem(item) { 31 | const itemGroup = new ItemGroup(item, 1); 32 | this.addItems(itemGroup); 33 | } 34 | 35 | addItems(itemGroup) { 36 | const amountInInventory = this.content[itemGroup.item.name] ? this.content[itemGroup.item.name].amount : 0; 37 | itemGroup.amount += amountInInventory; 38 | this.content[itemGroup.item.name] = itemGroup; 39 | } 40 | 41 | removeItem(item) { 42 | const itemGroup = new ItemGroup(item, 1); 43 | this.removeItems(itemGroup); 44 | } 45 | 46 | removeItems(itemGroup) { 47 | const amountInInventory = this.content[itemGroup.item.name] ? this.content[itemGroup.item.name].amount : 0; 48 | 49 | if (amountInInventory === itemGroup.amount) { 50 | delete this.content[itemGroup.item.name]; 51 | } else if (amountInInventory > itemGroup.amount) { 52 | itemGroup.amount = amountInInventory - itemGroup.amount; 53 | this.content[itemGroup.item.name] = itemGroup; 54 | } 55 | } 56 | 57 | save() { 58 | return Redis.db.hsetAsync('inventory', this.user, JSON.stringify(this.content)); 59 | } 60 | 61 | static fetchInventory(user) { 62 | return new Promise((resolve, reject) => { 63 | Redis.db.hgetAsync('inventory', user).then(content => { 64 | resolve(new Inventory(user, JSON.parse(content))); 65 | }).catch(reject); 66 | }); 67 | } 68 | } 69 | 70 | module.exports = Inventory; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commando", 3 | "version": "0.28.0", 4 | "description": ".", 5 | "author": { 6 | "name": "iCrawl", 7 | "email": "icrawltogo@gmail.com" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "vzwGrey" 12 | }, 13 | { 14 | "name": "1Computer" 15 | }, 16 | { 17 | "name": "aemino" 18 | }, 19 | { 20 | "name": "TeeSeal" 21 | }, 22 | { 23 | "name": "Lewdcario" 24 | }, 25 | { 26 | "name": "kurisubrooks" 27 | }, 28 | { 29 | "name": "Gawdl3y" 30 | }, 31 | { 32 | "name": "Slamakans" 33 | }, 34 | { 35 | "name": "ImSanctuary" 36 | }, 37 | { 38 | "name": "Drahcirius" 39 | } 40 | ], 41 | "license": "MIT", 42 | "scripts": { 43 | "test": "npm run lint", 44 | "lint": "eslint ." 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/WeebDev/Commando.git" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/WeebDev/Commando/issues" 52 | }, 53 | "keywords": [ 54 | "discord", 55 | "bot" 56 | ], 57 | "homepage": "https://github.com/WeebDev/Commando#readme", 58 | "dependencies": { 59 | "Sherlock": "neilgupta/Sherlock", 60 | "canvas": "next", 61 | "cheerio": "^0.22.0", 62 | "discord.js": "hydrabolt/discord.js", 63 | "discord.js-commando": "gawdl3y/discord.js-commando", 64 | "erlpack": "hammerandchisel/erlpack", 65 | "moment": "^2.0.0", 66 | "moment-duration-format": "^1.0.0", 67 | "node-opus": "^0.2.0", 68 | "pg": "^6.0.0", 69 | "pg-hstore": "^2.0.0", 70 | "prism-media": "hydrabolt/prism-media", 71 | "random-js": "^1.0.0", 72 | "redis": "^2.0.0", 73 | "request": "^2.0.0", 74 | "request-promise": "^4.0.0", 75 | "sequelize": "^4.0.0", 76 | "simple-youtube-api": "^3.0.0", 77 | "snekfetch": "^3.0.0", 78 | "tsubaki": "^1.0.0", 79 | "uws": "^8.0.0", 80 | "winston": "^2.0.0", 81 | "ytdl-core": "^0.17.0" 82 | }, 83 | "devDependencies": { 84 | "eslint": "^4.0.0", 85 | "eslint-config-aqua": "^2.0.0" 86 | }, 87 | "eslintConfig": { 88 | "extends": "aqua", 89 | "rules": { 90 | "no-process-env": "off" 91 | } 92 | }, 93 | "engines": { 94 | "node": ">=8.0.0" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /commands/util/search.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const cheerio = require('cheerio'); 3 | const snekfetch = require('snekfetch'); 4 | const querystring = require('querystring'); 5 | 6 | const { GOOGLE_CUSTOM_SEARCH, GOOGLE_CUSTOM_SEARCH_CX } = process.env; 7 | 8 | module.exports = class SearchCommand extends Command { 9 | constructor(client) { 10 | super(client, { 11 | name: 'search', 12 | aliases: ['s'], 13 | group: 'util', 14 | memberName: 'search', 15 | description: 'Searches google.', 16 | guildOnly: true, 17 | throttling: { 18 | usages: 2, 19 | duration: 3 20 | }, 21 | 22 | args: [ 23 | { 24 | key: 'search', 25 | prompt: 'what would you like to search the internet for?\n', 26 | type: 'string' 27 | } 28 | ] 29 | }); 30 | } 31 | 32 | async run(msg, { search }) { 33 | if (!GOOGLE_CUSTOM_SEARCH) return msg.reply('my Commander has not set the Google API Key. Go yell at him.'); 34 | if (!GOOGLE_CUSTOM_SEARCH_CX) return msg.reply('my Commander has not set the Google API Key. Go yell at him.'); 35 | 36 | const queryParams = { 37 | key: GOOGLE_CUSTOM_SEARCH, 38 | cx: GOOGLE_CUSTOM_SEARCH_CX, 39 | safe: 'medium', 40 | q: encodeURI(search) // eslint-disable-line id-length 41 | }; 42 | 43 | try { 44 | const res = await snekfetch.get(`https://www.googleapis.com/customsearch/v1?${querystring.stringify(queryParams)}`); // eslint-disable-line max-len 45 | if (res.body.queries.request[0].totalResults === '0') throw new Error('No results'); 46 | return msg.embed({ 47 | title: res.body.items[0].title, 48 | url: res.body.items[0].link, 49 | description: res.body.items[0].snippet, 50 | thumbnail: { url: res.body.items[0].pagemap.cse_image[0].src } 51 | }); 52 | } catch (error) { 53 | const res = await snekfetch.get(`https://www.google.com/search?safe=medium&q=${encodeURI(search)}`); 54 | const $ = cheerio.load(res.text); // eslint-disable-line id-length 55 | let href = $('.r') 56 | .first() 57 | .find('a') 58 | .first() 59 | .attr('href'); 60 | const title = $('.r') 61 | .first() 62 | .find('a') 63 | .text(); 64 | const description = $('.st') 65 | .first() 66 | .text(); 67 | if (!href) return msg.say('No results'); 68 | href = querystring.parse(href.replace('/url?', '')); 69 | return msg.embed({ 70 | title, 71 | url: href.q, 72 | description 73 | }); 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /commands/util/strawpoll.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const request = require('request-promise'); 3 | const { oneLine, stripIndents } = require('common-tags'); 4 | 5 | const { version } = require('../../package'); 6 | 7 | module.exports = class StrawpollCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'strawpoll', 11 | group: 'util', 12 | memberName: 'strawpoll', 13 | description: 'Create a strawpoll.', 14 | details: stripIndents`Create a strawpoll. 15 | The first argument is always the title, if you provde it, otherwise your username will be used! 16 | If you need to use spaces in your title make sure you put them in SingleQuotes => \`'topic here'\``, 17 | guildOnly: true, 18 | throttling: { 19 | usages: 2, 20 | duration: 3 21 | }, 22 | 23 | args: [ 24 | { 25 | key: 'title', 26 | prompt: 'what title would you like the strawpoll to have?\n', 27 | type: 'string', 28 | validate: title => { 29 | if (title.length > 200) { 30 | return ` 31 | your title was ${title.length} characters long. 32 | Please limit your title to 200 characters. 33 | `; 34 | } 35 | return true; 36 | } 37 | }, 38 | { 39 | key: 'options', 40 | prompt: oneLine` 41 | what options would you like to have? 42 | Every message you send will be interpreted as a single option.\n 43 | `, 44 | type: 'string', 45 | validate: option => { 46 | if (option.length > 160) { 47 | return ` 48 | your option was ${option.length} characters long. 49 | Please limit your option to 160 characters. 50 | `; 51 | } 52 | return true; 53 | }, 54 | infinite: true 55 | } 56 | ] 57 | }); 58 | } 59 | 60 | async run(msg, { title, options }) { 61 | if (options.length < 2) return msg.reply('please provide 2 or more options.'); 62 | if (options.length > 31) return msg.reply('please provide less than 31 options.'); 63 | 64 | const response = await request({ 65 | method: 'POST', 66 | uri: `https://strawpoll.me/api/v2/polls`, 67 | followAllRedirects: true, 68 | headers: { 'User-Agent': `Commando v${version} (https://github.com/WeebDev/Commando/)` }, 69 | body: { 70 | title, 71 | options, 72 | captcha: true 73 | }, 74 | json: true 75 | }); 76 | 77 | return msg.say(stripIndents`🗳 ${response.title} 78 | 79 | `); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /commands/economy/trade.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | 6 | module.exports = class MoneyTradeCommand extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'trade', 10 | aliases: [ 11 | 'trade-money', 12 | 'trade-donut', 13 | 'trade-donuts', 14 | 'trade-doughnut', 15 | 'trade-doughnuts', 16 | 'money-trade', 17 | 'donut-trade', 18 | 'donuts-trade', 19 | 'doughnut-trade', 20 | 'doughnuts-trade' 21 | ], 22 | group: 'economy', 23 | memberName: 'trade', 24 | description: `Trades the ${Currency.textPlural} you have earned.`, 25 | details: `Trades the amount of ${Currency.textPlural} you have earned.`, 26 | guildOnly: true, 27 | throttling: { 28 | usages: 2, 29 | duration: 3 30 | }, 31 | 32 | args: [ 33 | { 34 | key: 'member', 35 | prompt: `what user would you like to give ${Currency.textPlural}?\n`, 36 | type: 'member' 37 | }, 38 | { 39 | key: 'donuts', 40 | label: 'amount of donuts to trade', 41 | prompt: `how many ${Currency.textPlural} do you want to give that user?\n`, 42 | validate: donuts => /^(?:\d+|all|-all|-a)$/g.test(donuts), 43 | parse: async (donuts, msg) => { 44 | const balance = await Currency.getBalance(msg.author.id); 45 | 46 | if (['all', '-all', '-a'].includes(donuts)) return parseInt(balance); 47 | 48 | return parseInt(donuts); 49 | } 50 | } 51 | ] 52 | }); 53 | } 54 | 55 | async run(msg, { member, donuts }) { 56 | if (member.id === msg.author.id) return msg.reply(`you can't trade ${Currency.textPlural} with yourself, ya dingus.`); // eslint-disable-line 57 | if (member.user.bot) return msg.reply(`don't give your ${Currency.textPlural} to bots: they're bots, man.`); 58 | if (donuts <= 0) return msg.reply(`you can't trade 0 or less ${Currency.convert(0)}.`); 59 | 60 | const userBalance = await Currency.getBalance(msg.author.id); 61 | if (userBalance < donuts) { 62 | return msg.reply(stripIndents` 63 | you don't have that many ${Currency.textPlural} to trade! 64 | You currently have ${Currency.convert(userBalance)} on hand. 65 | `); 66 | } 67 | 68 | Currency.removeBalance(msg.author.id, donuts); 69 | Currency.addBalance(member.id, donuts); 70 | 71 | return msg.reply(`${member.displayName} successfully received your ${Currency.convert(donuts)}!`); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /commands/item/buy.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | const Inventory = require('../../structures/currency/Inventory'); 6 | const ItemGroup = require('../../structures/currency/ItemGroup'); 7 | const Store = require('../../structures/currency/Store'); 8 | 9 | module.exports = class BuyItemCommand extends Command { 10 | constructor(client) { 11 | super(client, { 12 | name: 'buy-item', 13 | aliases: ['item-buy', 'buy'], 14 | group: 'item', 15 | memberName: 'buy', 16 | description: 'Buys an item at the store.', 17 | details: 'Let\'s you exchange your hard earned donuts for other goods.', 18 | throttling: { 19 | usages: 2, 20 | duration: 3 21 | }, 22 | 23 | args: [ 24 | { 25 | key: 'item', 26 | prompt: 'what do you want to buy?\n', 27 | type: 'string', 28 | parse: str => str.toLowerCase() 29 | }, 30 | { 31 | key: 'amount', 32 | label: 'amount to buy', 33 | prompt: 'how many do you want to buy?\n', 34 | type: 'integer', 35 | default: 1, 36 | min: 1 37 | } 38 | ] 39 | }); 40 | } 41 | 42 | async run(msg, { amount, item }) { 43 | const itemName = item.replace(/(\b\w)/gi, lc => lc.toUpperCase()); 44 | const storeItem = Store.getItem(item); 45 | if (!storeItem) { 46 | return msg.reply(stripIndents` 47 | that item does not exist. 48 | 49 | You can use ${this.client.registry.commands.get('store').usage()} to get a list of the available items. 50 | `); 51 | } 52 | 53 | const balance = await Currency.getBalance(msg.author.id); 54 | const plural = amount > 1 || amount === 0; 55 | if (balance < storeItem.price * amount) { 56 | /* eslint-disable max-len */ 57 | return msg.reply(stripIndents` 58 | you don't have enough donuts to buy ${amount} ${itemName}${plural ? 's' : ''}. ${amount} ${itemName}${plural ? 's' : ''} cost${plural ? '' : 's'} ${amount * storeItem.price} 🍩s. 59 | Your current account balance is ${balance} 🍩s. 60 | `); 61 | /* eslint-enable max-len */ 62 | } 63 | 64 | const inventory = await Inventory.fetchInventory(msg.author.id); 65 | inventory.addItems(new ItemGroup(storeItem, amount)); 66 | Currency.removeBalance(msg.author.id, amount * storeItem.price); 67 | inventory.save(); 68 | 69 | return msg.reply(stripIndents` 70 | you have successfully purchased ${amount} ${itemName}${plural ? 's' : ''} for ${amount * storeItem.price} 🍩s. 71 | `); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /structures/currency/Bank.js: -------------------------------------------------------------------------------- 1 | const Currency = require('./Currency'); 2 | const Redis = require('../Redis'); 3 | 4 | // Rate * convert to decimal 5 | const INTEREST_MATURE_RATE = 0.1; 6 | const UPDATE_DURATION = 30 * 60 * 1000; 7 | const MIN_INTEREST_RATE = 0.001; 8 | 9 | Redis.db.getAsync('bankupdate').then(update => { 10 | setTimeout(() => Bank.applyInterest(), Math.max(0, (new Date(update) + UPDATE_DURATION) - Date.now())); 11 | }); 12 | 13 | class Bank { 14 | static changeLedger(user, amount) { 15 | Redis.db.hgetAsync('ledger', user).then(async balance => { 16 | const bal = parseInt(balance) || 0; 17 | await Redis.db.hsetAsync('ledger', user, amount + parseInt(bal)); 18 | }); 19 | } 20 | 21 | static async getBalance(user) { 22 | const balance = await Redis.db.hgetAsync('ledger', user) || 0; 23 | 24 | return parseInt(balance); 25 | } 26 | 27 | static deposit(user, amount) { 28 | Currency.removeBalance(user, amount); 29 | this.changeLedger(user, amount); 30 | } 31 | 32 | static withdraw(user, amount) { 33 | Currency.addBalance(user, amount); 34 | this.changeLedger(user, -amount); 35 | } 36 | 37 | static async applyInterest() { 38 | const interestRate = await this.getInterestRate(); 39 | const bankBalance = await Currency.getBalance('bank'); 40 | const previousBankBalance = await Redis.db.getAsync('lastbankbalance') || bankBalance; 41 | const bankBalanceDelta = (bankBalance - previousBankBalance) / previousBankBalance; 42 | 43 | Redis.db.hgetallAsync('ledger').then(async balances => { 44 | if (!balances) return; 45 | 46 | /* eslint-disable no-await-in-loop */ 47 | for (const [user, balance] of Object.entries(balances)) { 48 | await Redis.db.hsetAsync('ledger', user, Math.round(balance * (interestRate + 1))); 49 | } 50 | /* eslint-enable no-await-in-loop */ 51 | }); 52 | 53 | const newInterestRate = Math.max(MIN_INTEREST_RATE, interestRate + (bankBalanceDelta * -INTEREST_MATURE_RATE)); 54 | await Redis.db.setAsync('interestrate', newInterestRate); 55 | await Redis.db.setAsync('lastbankbalance', bankBalance); 56 | await Redis.db.setAsync('bankupdate', Date.now()); 57 | 58 | setTimeout(() => Bank.applyInterest(), UPDATE_DURATION); 59 | } 60 | 61 | static async getInterestRate() { 62 | const interestRate = await Redis.db.getAsync('interestrate') || 0.01; 63 | 64 | return parseFloat(interestRate); 65 | } 66 | 67 | static async nextUpdate() { 68 | const lastUpdate = await Redis.db.getAsync('bankupdate'); 69 | 70 | return UPDATE_DURATION - (Date.now() - lastUpdate); 71 | } 72 | } 73 | 74 | module.exports = Bank; 75 | -------------------------------------------------------------------------------- /commands/music/queue.js: -------------------------------------------------------------------------------- 1 | const { Command, util } = require('discord.js-commando'); 2 | const { oneLine, stripIndents } = require('common-tags'); 3 | 4 | const { PAGINATED_ITEMS } = process.env; 5 | const Song = require('../../structures/Song'); 6 | 7 | module.exports = class ViewQueueCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'queue', 11 | aliases: ['songs', 'song-list'], 12 | group: 'music', 13 | memberName: 'queue', 14 | description: 'Lists the queued songs.', 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'page', 24 | prompt: 'what page would you like to view?\n', 25 | type: 'integer', 26 | default: 1 27 | } 28 | ] 29 | }); 30 | } 31 | 32 | run(msg, { page }) { 33 | const queue = this.queue.get(msg.guild.id); 34 | if (!queue) return msg.reply('there are no songs in the queue. Why not start the party yourself?'); 35 | 36 | const paginated = util.paginate(queue.songs, page, Math.floor(PAGINATED_ITEMS)); 37 | const totalLength = queue.songs.reduce((prev, song) => prev + song.length, 0); 38 | const currentSong = queue.songs[0]; 39 | const currentTime = currentSong.dispatcher ? currentSong.dispatcher.time / 1000 : 0; 40 | 41 | return msg.embed({ 42 | color: 3447003, 43 | author: { 44 | name: `${msg.author.tag} (${msg.author.id})`, 45 | icon_url: msg.author.displayAvatarURL({ format: 'png' }) // eslint-disable-line camelcase 46 | }, 47 | /* eslint-disable max-len */ 48 | description: stripIndents` 49 | __**Song queue, page ${paginated.page}**__ 50 | ${paginated.items.map(song => `**-** ${!isNaN(song.id) ? `${song.name} (${song.lengthString})` : `[${song.name}](${`https://www.youtube.com/watch?v=${song.id}`})`} (${song.lengthString})`).join('\n')} 51 | ${paginated.maxPage > 1 ? `\nUse ${msg.usage()} to view a specific page.\n` : ''} 52 | 53 | **Now playing:** ${!isNaN(currentSong.id) ? `${currentSong.name}` : `[${currentSong.name}](${`https://www.youtube.com/watch?v=${currentSong.id}`})`} 54 | ${oneLine` 55 | **Progress:** 56 | ${!currentSong.playing ? 'Paused: ' : ''}${Song.timeString(currentTime)} / 57 | ${currentSong.lengthString} 58 | (${currentSong.timeLeft(currentTime)} left) 59 | `} 60 | **Total queue time:** ${Song.timeString(totalLength)} 61 | ` 62 | /* eslint-enable max-len */ 63 | }); 64 | } 65 | 66 | get queue() { 67 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 68 | 69 | return this._queue; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /commands/util/translate.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const request = require('request-promise'); 3 | 4 | const { SHERLOCK_API } = process.env; 5 | const { version } = require('../../package'); 6 | 7 | module.exports = class TranslateCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'translate', 11 | aliases: ['t'], 12 | group: 'util', 13 | memberName: 'translate', 14 | description: 'Translates the input text into the specified output language.', 15 | throttling: { 16 | usages: 5, 17 | duration: 60 18 | }, 19 | 20 | args: [ 21 | { 22 | key: 'query', 23 | prompt: 'what text do you want to translate?\n', 24 | type: 'string' 25 | }, 26 | { 27 | key: 'to', 28 | prompt: 'what language would you want to translate to?\n', 29 | type: 'string' 30 | }, 31 | { 32 | key: 'from', 33 | prompt: 'what language would you want to translate from?\n', 34 | type: 'string', 35 | default: '' 36 | } 37 | ] 38 | }); 39 | } 40 | 41 | async run(msg, { query, to, from }) { 42 | if (!SHERLOCK_API) return msg.reply('my Commander has not set the Sherlock API Key. Go yell at him.'); 43 | 44 | let response; 45 | try { 46 | response = await request({ 47 | method: 'POST', 48 | headers: { 49 | 'User-Agent': `Commando v${version} (https://github.com/WeebDev/Commando/)`, 50 | Authorization: SHERLOCK_API 51 | }, 52 | uri: `https://api.kurisubrooks.com/api/translate`, 53 | body: { to, from, query }, 54 | json: true 55 | }); 56 | } catch (error) { 57 | if (error.error) return msg.reply(this.handleError(error.error)); 58 | } 59 | 60 | return msg.embed({ 61 | author: { 62 | name: msg.member ? msg.member.displayName : msg.author.username, 63 | icon_url: msg.author.displayAvatarURL({ format: 'png' }) // eslint-disable-line camelcase 64 | }, 65 | fields: [ 66 | { 67 | name: response.from.name, 68 | value: response.query 69 | }, 70 | { 71 | name: response.to.name, 72 | value: response.result 73 | } 74 | ] 75 | }); 76 | } 77 | 78 | handleError(response) { 79 | if (response.error === 'Missing \'query\' field' || response.error === 'Missing \'to\' lang field') { 80 | return 'Required Fields are missing!'; 81 | } else if (response.error === 'Unknown \'to\' Language') { 82 | return 'Unknown \'to\' Language.'; 83 | } else if (response.error === 'Unknown \'from\' Language') { 84 | return 'Unknown \'from\' Language.'; 85 | } else { 86 | return 'Internal Server Error.'; 87 | } 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /structures/currency/Currency.js: -------------------------------------------------------------------------------- 1 | const Redis = require('../Redis'); 2 | const UserProfile = require('../../models/UserProfile'); 3 | 4 | const UPDATE_DURATION = 30 * 60 * 1000; 5 | 6 | Redis.db.hgetAsync('money', 'bank').then(async balance => { 7 | if (!balance) await Redis.db.hsetAsync('money', 'bank', 5000); 8 | }); 9 | 10 | class Currency { 11 | static _changeBalance(user, amount) { 12 | Redis.db.hgetAsync('money', user).then(balance => { 13 | const bal = parseInt(balance) || 0; 14 | 15 | return Redis.db.hsetAsync('money', user, amount + parseInt(bal)); 16 | }); 17 | } 18 | 19 | static changeBalance(user, amount) { 20 | Currency._changeBalance(user, amount); 21 | Currency._changeBalance('bank', -amount); 22 | } 23 | 24 | static addBalance(user, amount) { 25 | Currency.changeBalance(user, amount); 26 | } 27 | 28 | static removeBalance(user, amount) { 29 | Currency.changeBalance(user, -amount); 30 | } 31 | 32 | static async getBalance(user) { 33 | const money = await Redis.db.hgetAsync('money', user) || 0; 34 | 35 | return parseInt(money); 36 | } 37 | 38 | static async leaderboard() { 39 | const balances = await Redis.db.hgetallAsync('money') || {}; 40 | const bankBalances = await Redis.db.hgetallAsync('ledger') || {}; 41 | 42 | const ids = Object.keys(balances || {}); 43 | 44 | /* eslint-disable no-await-in-loop */ 45 | for (const id of ids) { 46 | const money = parseInt(balances[id] || 0); 47 | const balance = parseInt(bankBalances[id] || 0); 48 | const networth = money + balance; 49 | 50 | const user = await UserProfile.findOne({ where: { userID: id } }); 51 | if (!user) { 52 | await UserProfile.create({ 53 | userID: id, 54 | money, 55 | balance, 56 | networth 57 | }); 58 | } else { 59 | user.update({ 60 | money, 61 | balance, 62 | networth 63 | }); 64 | } 65 | } 66 | /* eslint-enable no-await-in-loop */ 67 | 68 | await Redis.db.setAsync('moneyleaderboardreset', Date.now()); 69 | setTimeout(() => Currency.leaderboard(), UPDATE_DURATION); 70 | } 71 | 72 | static convert(amount, text = false) { 73 | if (isNaN(amount)) amount = parseInt(amount); 74 | if (!text) return `${amount.toLocaleString()} ${Math.abs(amount) === 1 ? Currency.singular : Currency.plural}`; 75 | 76 | return `${amount.toLocaleString()} ${Math.abs(amount) === 1 ? Currency.textSingular : Currency.textPlural}`; 77 | } 78 | 79 | static get singular() { 80 | return '🧀'; 81 | } 82 | 83 | static get plural() { 84 | return '🧀'; 85 | } 86 | 87 | static get textSingular() { 88 | return 'cheese'; 89 | } 90 | 91 | static get textPlural() { 92 | return 'cheese'; 93 | } 94 | } 95 | 96 | module.exports = Currency; 97 | -------------------------------------------------------------------------------- /structures/games/Blackjack.js: -------------------------------------------------------------------------------- 1 | const decks = new Map(); 2 | const games = new Map(); 3 | 4 | const ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']; 5 | const suits = ['♣', '♦', '❤', '♠']; 6 | const DECK_TEMPLATE = suits 7 | .map(suit => ranks.concat(ranks) 8 | .concat(ranks) 9 | .concat(ranks) 10 | .map(rank => rank + suit)) 11 | .reduce((array, arr) => array.concat(arr)); 12 | 13 | class Blackjack { 14 | constructor(msg) { 15 | this.guildID = msg.guild.id; 16 | this.playerID = msg.author.id; 17 | 18 | games.set(this.playerID, this); 19 | } 20 | 21 | getHand() { 22 | return this.hit(this.hit([])); 23 | } 24 | 25 | hit(hand) { 26 | if (!this.deck || this.deck.length === 0) { 27 | if (decks.has(this.guildID) && decks.get(this.guildID).length !== 0) { 28 | this.deck = decks.get(this.guildID); 29 | } else { 30 | this.deck = Blackjack._shuffle(DECK_TEMPLATE); 31 | decks.set(this.guildID, this.deck); 32 | } 33 | } 34 | hand.push(this.deck.pop()); 35 | 36 | return hand; 37 | } 38 | 39 | endGame() { 40 | return games.delete(this.playerID); 41 | } 42 | 43 | cardsRemaining() { 44 | return decks.has(this.guildID) ? decks.get(this.guildID).length : this.decks.length; 45 | } 46 | 47 | static gameExists(playerID) { 48 | return games.has(playerID); 49 | } 50 | 51 | static isSoft(hand) { 52 | let value = 0; 53 | let aces = 0; 54 | 55 | hand.forEach(card => { 56 | value += Blackjack._cardValue(card); 57 | if (Blackjack._cardValue(card) === 11) aces++; 58 | }); 59 | 60 | while (value > 21 && aces > 0) { 61 | value -= 10; 62 | aces--; 63 | } 64 | 65 | if (value === 21 && hand.length === 2) return false; 66 | 67 | return aces !== 0; 68 | } 69 | 70 | static handValue(hand) { 71 | let value = 0; 72 | let aces = 0; 73 | 74 | hand.forEach(card => { 75 | value += Blackjack._cardValue(card); 76 | if (Blackjack._cardValue(card) === 11) aces++; 77 | }); 78 | 79 | while (value > 21 && aces > 0) { 80 | value -= 10; 81 | aces--; 82 | } 83 | 84 | if (value === 21 && hand.length === 2) return 'Blackjack'; 85 | 86 | return value; 87 | } 88 | 89 | static _cardValue(card) { 90 | const index = ranks.indexOf(card.slice(0, -1)); 91 | if (index === 0) return 11; 92 | 93 | return index >= 10 ? 10 : index + 1; 94 | } 95 | 96 | static _shuffle(array) { 97 | const newArray = array.slice(); 98 | for (let i = array.length - 1; i > 0; i--) { 99 | const j = Math.floor(Math.random() * (i + 1)); 100 | const temp = newArray[i]; 101 | newArray[i] = newArray[j]; 102 | newArray[j] = temp; 103 | } 104 | 105 | return newArray; 106 | } 107 | } 108 | 109 | module.exports = Blackjack; 110 | -------------------------------------------------------------------------------- /commands/economy/leaderboard.js: -------------------------------------------------------------------------------- 1 | const { oneLine, stripIndents } = require('common-tags'); 2 | const { Command, util } = require('discord.js-commando'); 3 | const moment = require('moment'); 4 | const Sequelize = require('sequelize'); 5 | 6 | const Currency = require('../../structures/currency/Currency'); 7 | const { PAGINATED_ITEMS } = process.env; 8 | const UserProfile = require('../../models/UserProfile'); 9 | 10 | module.exports = class MoneyLeaderboardCommand extends Command { 11 | constructor(client) { 12 | super(client, { 13 | name: 'leaderboard', 14 | aliases: [ 15 | 'baltop', 16 | 'balancetop', 17 | 'money-leaderboard', 18 | 'donut-leaderboard', 19 | 'donuts-leaderboard', 20 | 'doughnut-leaderboard', 21 | 'doughnuts-leaderboard' 22 | ], 23 | group: 'economy', 24 | memberName: 'leaderboard', 25 | description: `Displays the ${Currency.textPlural} members have earned.`, 26 | details: `Display the amount of ${Currency.textPlural} members have earned in a leaderboard.`, 27 | guildOnly: true, 28 | throttling: { 29 | usages: 2, 30 | duration: 3 31 | }, 32 | 33 | args: [ 34 | { 35 | key: 'page', 36 | prompt: 'what page would you like to view?\n', 37 | type: 'integer', 38 | default: 1 39 | } 40 | ] 41 | }); 42 | } 43 | 44 | async run(msg, { page }) { 45 | const lastUpdate = await this.client.redis.getAsync('moneyleaderboardreset'); 46 | const cooldown = 30 * 60 * 1000; 47 | const reset = cooldown - (Date.now() - lastUpdate); 48 | const money = await this.findCached(); 49 | const paginated = util.paginate(JSON.parse(money), page, Math.floor(PAGINATED_ITEMS)); 50 | let ranking = PAGINATED_ITEMS * (paginated.page - 1); 51 | 52 | for (const user of paginated.items) await this.client.users.fetch(user.userID); // eslint-disable-line 53 | 54 | return msg.embed({ 55 | color: 3447003, 56 | description: stripIndents` 57 | __**${Currency.textSingular.replace(/./, lc => lc.toUpperCase())} leaderboard, page ${paginated.page}**__ 58 | 59 | ${paginated.items.map(user => oneLine` 60 | **${++ranking} -** 61 | ${`${this.client.users.get(user.userID).tag}`} 62 | (**${Currency.convert(user.networth)}**)`).join('\n')} 63 | 64 | ${moment.duration(reset).format('hh [hours] mm [minutes]')} until the next update. 65 | `, 66 | footer: { text: paginated.maxPage > 1 ? `Use ${msg.usage()} to view a specific page.` : '' } 67 | }); 68 | } 69 | 70 | async findCached() { 71 | const cache = await this.client.redis.getAsync('moneyleaderboard'); 72 | const cacheExpire = await this.client.redis.ttlAsync('moneyleaderboard'); 73 | if (cacheExpire !== -1 && cacheExpire !== -2) return cache; 74 | 75 | const money = await UserProfile.findAll( 76 | { where: { userID: { $ne: 'bank' } }, order: Sequelize.literal('networth DESC') } 77 | ); 78 | 79 | await this.client.redis.setAsync('moneyleaderboard', JSON.stringify(money)); 80 | await this.client.redis.expireAsync('moneyleaderboard', 3600); 81 | 82 | return JSON.stringify(money); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /structures/games/Roulette.js: -------------------------------------------------------------------------------- 1 | const { Collection } = require('discord.js'); 2 | 3 | const games = new Map(); 4 | 5 | const roulette = { 6 | red: [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36], 7 | black: [2, 4, 6, 8, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 29, 31, 33, 35] 8 | }; 9 | 10 | const spaces = new Collection([ 11 | ['numbers', { values: roulette.red.concat(roulette.black).concat([0]).map(item => item.toString()), multiplier: 36 }], 12 | ['dozens', { values: ['1-12', '13-24', '25-36'], multiplier: 3 }], 13 | ['columns', { values: ['1st', '2nd', '3rd'], multiplier: 3 }], 14 | ['halves', { values: ['1-18', '19-36'], multiplier: 2 }], 15 | ['parity', { values: ['even', 'odd'], multiplier: 2 }], 16 | ['colors', { values: ['red', 'black'], multiplier: 2 }] 17 | ]); 18 | 19 | class Roulette { 20 | constructor(guildID) { 21 | this.guildID = guildID; 22 | this.players = []; 23 | this.winSpaces = Roulette._generateSpaces(); 24 | 25 | games.set(this.guildID, this); 26 | } 27 | 28 | join(user, donuts, space) { 29 | const multiplier = this.winSpaces.includes(space) 30 | ? spaces.find(spc => spc.values.includes(space)).multiplier 31 | : 0; 32 | this.players.push({ 33 | user, 34 | winnings: donuts * multiplier 35 | }); 36 | 37 | games.set(this.guildID, this); 38 | } 39 | 40 | hasPlayer(userID) { 41 | return !!this.players.find(player => player.user.id === userID); 42 | } 43 | 44 | awaitPlayers(time) { 45 | return new Promise(resolve => { 46 | setTimeout(() => { 47 | games.delete(this.guildID); 48 | return resolve(this.players || []); 49 | }, time); 50 | }); 51 | } 52 | 53 | static findGame(guildID) { 54 | return games.get(guildID) || null; 55 | } 56 | 57 | static hasSpace(space) { 58 | return !!spaces.find(spc => spc.values.includes(space)); 59 | } 60 | 61 | static _generateSpaces() { 62 | const winNumber = Math.floor(Math.random() * 37); 63 | 64 | return [ 65 | winNumber.toString(), 66 | Roulette._getColor(winNumber), 67 | Roulette._getRange(winNumber, 'dozens'), 68 | Roulette._getColumn(winNumber), 69 | Roulette._getRange(winNumber, 'halves'), 70 | Roulette._getParity(winNumber) 71 | ]; 72 | } 73 | 74 | static _getColor(number) { 75 | if (number === 0) return null; 76 | 77 | return roulette.red.includes(number) ? 'red' : 'black'; 78 | } 79 | 80 | static _getRange(number, size) { 81 | if (number === 0) return null; 82 | 83 | return spaces.get(size).values.find(value => { 84 | const min = parseInt(value.split('-')[0]); 85 | const max = parseInt(value.split('-')[1]); 86 | return number >= min && number <= max; 87 | }); 88 | } 89 | 90 | static _getColumn(number) { 91 | if (number === 0) return null; 92 | 93 | return spaces.get('columns').values[(number - 1) % 3]; 94 | } 95 | 96 | static _getParity(number) { 97 | if (number === 0) return null; 98 | 99 | return spaces.get('parity').values[number % 2]; 100 | } 101 | } 102 | 103 | module.exports = Roulette; 104 | -------------------------------------------------------------------------------- /commands/util/clean.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | module.exports = class CleanCommand extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'clean', 7 | aliases: ['purge', 'prune', 'clear'], 8 | group: 'util', 9 | memberName: 'clean', 10 | description: 'Deletes messages.', 11 | details: `Deletes messages. Here is a list of filters: 12 | __invites:__ Messages containing an invite 13 | __user @user:__ Messages sent by @user 14 | __bots:__ Messages sent by bots 15 | __you:__ Messages sent by Commando 16 | __uploads:__ Messages containing an attachment 17 | __links:__ Messages containing a link`, 18 | guildOnly: true, 19 | throttling: { 20 | usages: 2, 21 | duration: 3 22 | }, 23 | 24 | args: [ 25 | { 26 | key: 'limit', 27 | prompt: 'how many messages would you like to delete?\n', 28 | type: 'integer', 29 | max: 100 30 | }, 31 | { 32 | key: 'filter', 33 | prompt: 'what filter would you like to apply?\n', 34 | type: 'string', 35 | default: '', 36 | parse: str => str.toLowerCase() 37 | }, 38 | { 39 | key: 'member', 40 | prompt: 'whose messages would you like to delete?\n', 41 | type: 'member', 42 | default: '' 43 | } 44 | ] 45 | }); 46 | } 47 | 48 | hasPermission(msg) { 49 | return this.client.isOwner(msg.author) || msg.member.roles.exists('name', 'Server Staff'); 50 | } 51 | 52 | async run(msg, { filter, limit, member }) { 53 | let messageFilter; 54 | 55 | if (filter) { 56 | if (filter === 'invite') { 57 | messageFilter = message => message.content.search(/(discord\.gg\/.+|discordapp\.com\/invite\/.+)/i) 58 | !== -1; 59 | } else if (filter === 'user') { 60 | if (member) { 61 | const { user } = member; 62 | messageFilter = message => message.author.id === user.id; 63 | } else { 64 | return msg.say(`${msg.author}, you have to mention someone.`); 65 | } 66 | } else if (filter === 'bots') { 67 | messageFilter = message => message.author.bot; 68 | } else if (filter === 'you') { 69 | messageFilter = message => message.author.id === this.client.user.id; 70 | } else if (filter === 'upload') { 71 | messageFilter = message => message.attachments.size !== 0; 72 | } else if (filter === 'links') { 73 | messageFilter = message => message.content.search(/https?:\/\/[^ \/\.]+\.[^ \/\.]+/) !== -1; // eslint-disable-line no-useless-escape, max-len 74 | } else { 75 | return msg.say(`${msg.author}, this is not a valid filter. \`help clean\` for all available filters.`); 76 | } 77 | 78 | /* eslint-disable no-unused-vars, handle-callback-err */ 79 | const messages = await msg.channel.messages.fetch({ limit }).catch(err => null); 80 | const messagesToDelete = messages.filter(messageFilter); 81 | msg.channel.bulkDelete(messagesToDelete.array().reverse()).catch(err => null); 82 | 83 | return null; 84 | } 85 | 86 | const messagesToDelete = await msg.channel.messages.fetch({ limit }).catch(err => null); 87 | msg.channel.bulkDelete(messagesToDelete.array().reverse()).catch(err => null); 88 | 89 | return null; 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /commands/games/russian-roulette.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | const RussianRoulette = require('../../structures/games/RussianRoulette'); 6 | 7 | module.exports = class RussianRouletteCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'russian-roulette', 11 | aliases: ['rus-roulette'], 12 | group: 'games', 13 | memberName: 'russian-roulette', 14 | description: `Play a game of russian roulette for ${Currency.textPlural}!`, 15 | details: `Play a game of russian roulette for ${Currency.textPlural}.`, 16 | guildOnly: true, 17 | throttling: { 18 | usages: 1, 19 | duration: 30 20 | } 21 | }); 22 | } 23 | 24 | async run(msg) { 25 | const donuts = 120; 26 | const balance = await Currency.getBalance(msg.author.id); 27 | let roulette = RussianRoulette.findGame(msg.guild.id); 28 | if (balance < donuts) { 29 | return msg.reply(stripIndents` 30 | you don't have enough ${Currency.textPlural}. 31 | Your current account balance is ${Currency.convert(balance)}. 32 | You need ${Currency.convert(donuts)} to join. 33 | `); 34 | } 35 | 36 | if (roulette) { 37 | if (roulette.hasPlayer(msg.author.id)) { 38 | return msg.reply('you have already joined this game of russian roulette.'); 39 | } 40 | 41 | if (roulette.players.length === 6) { 42 | return msg.reply('only 6 people can join at a time. You\'ll have to wait for the next round'); 43 | } 44 | 45 | roulette.join(msg.author, donuts); 46 | 47 | return msg.reply('you have successfully joined the game.'); 48 | } 49 | 50 | roulette = new RussianRoulette(msg.guild.id); 51 | roulette.join(msg.author, donuts); 52 | 53 | const barrel = this.generateBarrel(); 54 | 55 | return msg.say(stripIndents` 56 | A new game of russian roulette has been initiated! 57 | 58 | Use the ${msg.usage()} command in the next 15 seconds to join! 59 | `).then(async () => { 60 | setTimeout(() => msg.say('5 more seconds for new people to join'), 10000); 61 | setTimeout(() => { if (roulette.players.length > 1) msg.say('The game begins!'); }, 14500); 62 | 63 | const players = await roulette.awaitPlayers(15000); 64 | if (players.length === 1) { 65 | return msg.say('Seems like no one else wanted to join. Ah well, maybe another time.'); 66 | } 67 | 68 | let deadPlayer = null; 69 | let survivors = []; 70 | 71 | for (const slot in barrel) { 72 | let currentPlayer = players[slot % players.length]; 73 | if (!deadPlayer) deadPlayer = currentPlayer; 74 | } 75 | 76 | survivors = players.filter(player => player !== deadPlayer); 77 | Currency.removeBalance(deadPlayer.user.id, donuts); 78 | survivors.forEach(survivor => Currency.addBalance(survivor.user.id, donuts / survivors.length)); 79 | 80 | return msg.embed({ 81 | description: stripIndents` 82 | __**Survivors**__ 83 | ${survivors.map(survivor => survivor.user.username).join('\n')} 84 | 85 | __**Reward**__ 86 | The survivors receive ${Currency.convert(donuts / survivors.length)} each. 87 | ` 88 | }); 89 | }); 90 | } 91 | 92 | generateBarrel() { 93 | let barrel = [0, 0, 0, 0, 0, 0]; 94 | barrel[Math.floor(Math.random() * barrel.length)] = 1; 95 | 96 | return barrel; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /commands/games/roulette.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | const Roulette = require('../../structures/games/Roulette'); 6 | 7 | const colors = { 8 | red: 0xBE1931, 9 | black: 0x0C0C0C 10 | }; 11 | 12 | module.exports = class RouletteCommand extends Command { 13 | constructor(client) { 14 | super(client, { 15 | name: 'roulette', 16 | aliases: ['roulette'], 17 | group: 'games', 18 | memberName: 'roulette', 19 | description: `Play a game of roulette for ${Currency.textPlural}!`, 20 | details: `Play a game of roulette for ${Currency.textPlural}.`, 21 | guildOnly: true, 22 | throttling: { 23 | usages: 1, 24 | duration: 30 25 | }, 26 | 27 | args: [ 28 | { 29 | key: 'bet', 30 | prompt: `how many ${Currency.textPlural} do you want to bet?\n`, 31 | type: 'integer', 32 | validate: async (bet, msg) => { 33 | bet = parseInt(bet); 34 | const balance = await Currency.getBalance(msg.author.id); 35 | if (balance < bet) { 36 | return ` 37 | you don't have enough ${Currency.textPlural} to bet. 38 | Your current account balance is ${Currency.convert(balance)}. 39 | Please specify a valid amount of ${Currency.textPlural}. 40 | `; 41 | } 42 | 43 | if (![100, 200, 300, 400, 500, 1000, 2000, 5000].includes(bet)) { 44 | return ` 45 | please choose \`100, 200, 300, 400, 500, 1000, 2000, or 5000\` for your bet. 46 | `; 47 | } 48 | 49 | return true; 50 | } 51 | }, 52 | { 53 | key: 'space', 54 | prompt: 'On what space do you want to bet?', 55 | type: 'string', 56 | validate: space => { 57 | if (!Roulette.hasSpace(space)) { 58 | return ` 59 | That is not a valid betting space. Use \`roulette-info\` for more information. 60 | `; 61 | } 62 | 63 | return true; 64 | }, 65 | parse: str => str.toLowerCase() 66 | } 67 | ] 68 | }); 69 | } 70 | 71 | run(msg, { bet, space }) { 72 | let roulette = Roulette.findGame(msg.guild.id); 73 | if (roulette) { 74 | if (roulette.hasPlayer(msg.author.id)) { 75 | return msg.reply('you have already put a bet in this game of roulette.'); 76 | } 77 | roulette.join(msg.author, bet, space); 78 | Currency.removeBalance(msg.author.id, bet); 79 | 80 | return msg.reply(`you have successfully placed your bet of ${Currency.convert(bet)} on ${space}.`); 81 | } 82 | 83 | roulette = new Roulette(msg.guild.id); 84 | roulette.join(msg.author, bet, space); 85 | Currency.removeBalance(msg.author.id, bet); 86 | 87 | return msg.say(stripIndents` 88 | A new game of roulette has been initiated! 89 | 90 | Use ${msg.usage()} in the next 15 seconds to place your bet. 91 | `) 92 | .then(async () => { 93 | setTimeout(() => msg.say('5 more seconds for new people to bet.'), 10000); 94 | setTimeout(() => msg.say('The roulette starts spinning!'), 14500); 95 | 96 | const winners = await roulette.awaitPlayers(16000).filter(player => player.winnings !== 0); 97 | winners.forEach(winner => Currency.changeBalance(winner.user.id, winner.winnings)); 98 | 99 | /* eslint-disable max-len */ 100 | return msg.embed({ 101 | color: colors[roulette.winSpaces[1]] || null, 102 | description: stripIndents` 103 | The ball landed on: **${roulette.winSpaces[1] ? roulette.winSpaces[1] : ''} ${roulette.winSpaces[0]}**! 104 | 105 | ${winners.length !== 0 ? `__**Winners:**__ 106 | ${winners.map(winner => `${winner.user.username} won ${Currency.convert(winner.winnings)}`).join('\n')}` : '__**No winner.**__'} 107 | ` 108 | }); 109 | /* eslint-enable max-len */ 110 | }); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /commands/music/skip.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { oneLine } = require('common-tags'); 3 | 4 | module.exports = class SkipSongCommand extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'skip', 8 | group: 'music', 9 | memberName: 'skip', 10 | description: 'Skips the song that is currently playing.', 11 | details: oneLine` 12 | If there are 3 people or fewer (excluding the bot) in the voice channel, the skip will be immediate. 13 | With at least 4 people, a voteskip will be started with 15 seconds to accept votes. 14 | The required votes to successfully skip the song is one-third of the number of listeners, rounded up. 15 | Each vote will add 5 seconds to the vote's timer. 16 | Moderators can use the "force" parameter, which will immediately skip without a vote, no matter what. 17 | `, 18 | guildOnly: true, 19 | throttling: { 20 | usages: 2, 21 | duration: 3 22 | } 23 | }); 24 | 25 | this.votes = new Map(); 26 | } 27 | 28 | run(msg, args) { 29 | const queue = this.queue.get(msg.guild.id); 30 | if (!queue) return msg.reply('there isn\'t a song playing right now, silly.'); 31 | if (!queue.voiceChannel.members.has(msg.author.id)) { 32 | return msg.reply('you\'re not in the voice channel. You better not be trying to mess with their mojo, man.'); 33 | } 34 | if (!queue.songs[0].dispatcher) return msg.reply('the song hasn\'t even begun playing yet. Why not give it a chance?'); // eslint-disable-line max-len 35 | 36 | const threshold = Math.ceil((queue.voiceChannel.members.size - 1) / 3); 37 | const force = threshold <= 1 38 | || queue.voiceChannel.members.size < threshold 39 | || (msg.member.hasPermission('MANAGE_MESSAGES') 40 | && args.toLowerCase() === 'force'); 41 | if (force) return msg.reply(this.skip(msg.guild, queue)); 42 | 43 | const vote = this.votes.get(msg.guild.id); 44 | if (vote && vote.count >= 1) { 45 | if (vote.users.some(user => user === msg.author.id)) return msg.reply('you\'ve already voted to skip the song.'); 46 | 47 | vote.count++; 48 | vote.users.push(msg.author.id); 49 | if (vote.count >= threshold) return msg.reply(this.skip(msg.guild, queue)); 50 | 51 | const time = this.setTimeout(vote); 52 | const remaining = threshold - vote.count; 53 | 54 | return msg.say(oneLine` 55 | ${vote.count} vote${vote.count > 1 ? 's' : ''} received so far, 56 | ${remaining} more ${remaining > 1 ? 'are' : 'is'} needed to skip. 57 | Five more seconds on the clock! The vote will end in ${time} seconds. 58 | `); 59 | } else { 60 | const newVote = { 61 | count: 1, 62 | users: [msg.author.id], 63 | queue, 64 | guild: msg.guild.id, 65 | start: Date.now(), 66 | timeout: null 67 | }; 68 | 69 | const time = this.setTimeout(newVote); 70 | this.votes.set(msg.guild.id, newVote); 71 | const remaining = threshold - 1; 72 | 73 | return msg.say(oneLine` 74 | Starting a voteskip. 75 | ${remaining} more vote${remaining > 1 ? 's are' : ' is'} required for the song to be skipped. 76 | The vote will end in ${time} seconds. 77 | `); 78 | } 79 | } 80 | 81 | skip(guild, queue) { 82 | if (this.votes.has(guild.id)) { 83 | clearTimeout(this.votes.get(guild.id).timeout); 84 | this.votes.delete(guild.id); 85 | } 86 | 87 | const song = queue.songs[0]; 88 | song.dispatcher.end(); 89 | 90 | return `Skipped: **${song}**`; 91 | } 92 | 93 | setTimeout(vote) { 94 | const time = vote.start + 15000 - Date.now() + ((vote.count - 1) * 5000); 95 | clearTimeout(vote.timeout); 96 | vote.timeout = setTimeout(() => { 97 | this.votes.delete(vote.guild); 98 | vote.queue.textChannel.send('The vote to skip the current song has ended. Get outta here, party poopers.'); 99 | }, time); 100 | 101 | return Math.round(time / 1000); 102 | } 103 | 104 | get queue() { 105 | if (!this._queue) this._queue = this.client.registry.resolveCommand('music:play').queue; 106 | 107 | return this._queue; 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /commands/games/slot-machine.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Currency = require('../../structures/currency/Currency'); 5 | const Inventory = require('../../structures/currency/Inventory'); 6 | const ItemGroup = require('../../structures/currency/ItemGroup'); 7 | const Store = require('../../structures/currency/Store'); 8 | 9 | const combinations = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 4, 8], [2, 4, 6]]; 10 | const reels = [ 11 | ['🍒', '💰', '⭐', '🎲', '💎', '❤', '⚜', '🔅', '🎉'], 12 | ['💎', '🔅', '❤', '🍒', '🎉', '⚜', '🎲', '⭐', '💰'], 13 | ['❤', '🎲', '💎', '⭐', '⚜', '🍒', '💰', '🎉', '🔅'] 14 | ]; 15 | 16 | const values = { 17 | '💎': 500, 18 | '⚜': 400, 19 | '💰': 400, 20 | '❤': 300, 21 | '⭐': 300, 22 | '🎲': 250, 23 | '🔅': 250, 24 | '🎉': 250, 25 | '🍒': 250 26 | }; 27 | 28 | module.exports = class SlotMachineCommand extends Command { 29 | constructor(client) { 30 | super(client, { 31 | name: 'slot-machine', 32 | aliases: ['slot', 'slots'], 33 | group: 'games', 34 | memberName: 'slot-machine', 35 | description: 'Let\'s you play a round with the slot machine', 36 | details: stripIndents` 37 | Bet some amount of coins, and enjoy a round with the slot machine. 38 | `, 39 | throttling: { 40 | usages: 1, 41 | duration: 5 42 | }, 43 | 44 | args: [ 45 | { 46 | key: 'coins', 47 | label: 'amount of coins', 48 | prompt: 'how many coins do you want to bet?\n', 49 | type: 'integer', 50 | validate: async (coins, msg) => { 51 | coins = parseInt(coins); 52 | const inventory = await Inventory.fetchInventory(msg.author.id); 53 | const userCoins = (inventory.content.coin || { amount: 0 }).amount; 54 | const plural = userCoins > 1 || userCoins === 0; 55 | if (userCoins < coins) { 56 | return ` 57 | you don't have enough coins to pay your bet! 58 | Your current account balance is ${userCoins} coin${plural ? 's' : ''}. 59 | Please specify a valid amount of coins. 60 | `; 61 | } 62 | 63 | if (![1, 3, 5].includes(coins)) { 64 | return ` 65 | you need to pay either 1, 3 or 5 coin(s). 66 | `; 67 | } 68 | 69 | return true; 70 | } 71 | } 72 | ] 73 | }); 74 | } 75 | 76 | async run(msg, { coins }) { 77 | const inventory = await Inventory.fetchInventory(msg.author.id); 78 | const item = Store.getItem('coin'); 79 | 80 | inventory.removeItems(new ItemGroup(item, coins)); 81 | inventory.save(); 82 | Currency.addBalance('bank', coins * 100); 83 | const roll = this.generateRoll(); 84 | let winnings = 0; 85 | 86 | combinations.forEach(combo => { 87 | if (roll[combo[0]] === roll[combo[1]] && roll[combo[1]] === roll[combo[2]]) { 88 | winnings += values[roll[combo[0]]]; 89 | } 90 | }); 91 | 92 | if (winnings === 0) { 93 | return msg.embed({ 94 | color: 0xBE1931, 95 | description: stripIndents` 96 | **${msg.member.displayName}, you rolled:** 97 | 98 | ${this.showRoll(roll)} 99 | 100 | **You lost!** 101 | Better luck next time! 102 | ` 103 | }); 104 | } 105 | 106 | Currency.addBalance(msg.author.id, coins * winnings); 107 | 108 | return msg.embed({ 109 | color: 0x5C913B, 110 | description: stripIndents` 111 | **${msg.member.displayName}, you rolled:** 112 | 113 | ${this.showRoll(roll)} 114 | 115 | **Congratulations!** 116 | You won ${Currency.convert(coins * winnings)}! 117 | ` 118 | }); 119 | } 120 | 121 | showRoll(roll) { 122 | return stripIndents` 123 | ${roll[0]}ー${roll[1]}ー${roll[2]} 124 | ${roll[3]}ー${roll[4]}ー${roll[5]} 125 | ${roll[6]}ー${roll[7]}ー${roll[8]} 126 | `; 127 | } 128 | 129 | generateRoll() { 130 | const roll = []; 131 | reels.forEach((reel, index) => { 132 | const rand = Math.floor(Math.random() * reel.length); 133 | roll[index] = rand === 0 ? reel[reel.length - 1] : reel[rand - 1]; 134 | roll[index + 3] = reel[rand]; 135 | roll[index + 6] = rand === reel.length - 1 ? reel[0] : reel[rand + 1]; 136 | }); 137 | 138 | return roll; 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /commands/util/cybernuke.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | const winston = require('winston'); 4 | 5 | module.exports = class LaunchCybernukeCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'launch-cybernuke', 9 | aliases: ['cybernuke'], 10 | group: 'util', 11 | memberName: 'cybernuke', 12 | description: 'Bans all members that have joined recently, with new accounts.', 13 | guildOnly: true, 14 | 15 | args: [ 16 | { 17 | key: 'join', 18 | label: 'member age', 19 | prompt: 'how old (in minutes) should a member be for the cybernuke to ignore them (server join date)?', 20 | type: 'float', 21 | min: 0.1, 22 | max: 120 23 | }, 24 | { 25 | key: 'age', 26 | label: 'account age', 27 | prompt: 'how old (in minutes) should a member\'s account be for the cybernuke to ignore them (account age)?', 28 | type: 'float', 29 | min: 0.1 30 | } 31 | ] 32 | }); 33 | } 34 | 35 | hasPermission(msg) { 36 | return this.client.isOwner(msg.author) || msg.member.hasPermission('ADMINISTRATOR'); 37 | } 38 | 39 | async run(msg, { age, join }) { 40 | const statusMsg = await msg.reply('Calculating targeting parameters for cybernuke...'); 41 | await msg.guild.members.fetch(); 42 | 43 | const memberCutoff = Date.now() - (join * 60000); 44 | const ageCutoff = Date.now() - (age * 60000); 45 | const members = msg.guild.members.filter( 46 | mem => mem.joinedTimestamp > memberCutoff && mem.user.createdTimestamp > ageCutoff 47 | ); 48 | const booleanType = this.client.registry.types.get('boolean'); 49 | 50 | await statusMsg.edit(`Cybernuke will strike ${members.size} members; proceed?`); 51 | let response; 52 | let statusMsg2; 53 | 54 | /* eslint-disable no-await-in-loop */ 55 | while (!statusMsg2) { 56 | const responses = await msg.channel.awaitMessages(msg2 => msg2.author.id === msg.author.id, { 57 | maxMatches: 1, 58 | time: 10000 59 | }); 60 | 61 | if (!responses || responses.size !== 1) { 62 | await msg.reply('Cybernuke cancelled.'); 63 | return null; 64 | } 65 | response = responses.first(); 66 | 67 | if (booleanType.validate(response.content)) { 68 | if (!booleanType.parse(response.content)) { 69 | await response.reply('Cybernuke cancelled.'); 70 | return null; 71 | } 72 | 73 | statusMsg2 = await response.reply('Launching cybernuke...'); 74 | } else { 75 | await response.reply(stripIndents` 76 | Unknown response. Please confirm the cybernuke launch with a simple "yes" or "no". 77 | Awaiting your input, commander... 78 | `); 79 | } 80 | } 81 | /* eslint-enable no-await-in-loop */ 82 | 83 | const fatalities = []; 84 | const survivors = []; 85 | const promises = []; 86 | 87 | for (const member of members.values()) { 88 | promises.push( 89 | member.send(stripIndents` 90 | Sorry, but you've been automatically targetted by the cybernuke in the "${msg.guild.name}" server. 91 | This means that you have been banned, likely in the case of a server raid. 92 | Please contact them if you believe this ban to be in error. 93 | `).catch(winston.error) 94 | .then(() => member.ban()) 95 | .then(() => { 96 | fatalities.push(member); 97 | }) 98 | .catch(err => { 99 | winston.error(err); 100 | survivors.push({ 101 | member: member.id, 102 | error: err 103 | }); 104 | }) 105 | .then(() => { 106 | if (members.size <= 5) return; 107 | if (promises.length % 5 === 0) { 108 | statusMsg2.edit(`Launching cybernuke (${Math.round(promises.length / members.size * 100)}%)...`); 109 | } 110 | }) 111 | ); 112 | } 113 | 114 | await Promise.all(promises); 115 | await statusMsg2.edit('Cybernuke impact confirmed. Casualty report incoming...'); 116 | await response.reply(stripIndents` 117 | __**Fatalities**__ 118 | ${fatalities.length > 0 ? stripIndents` 119 | ${fatalities.length} confirmed KIA. 120 | 121 | ${fatalities.map(fat => `**-** ${fat.displayName} (${fat.id})`).join('\n')} 122 | ` : 'None'} 123 | 124 | 125 | ${survivors.length > 0 ? stripIndents` 126 | __**Survivors**__ 127 | ${survivors.length} left standing. 128 | 129 | ${survivors.map(srv => `**-** ${srv.member.displayName} (${srv.member.id}): \`${srv.error}\``).join('\n')} 130 | ` : ''} 131 | `, { split: true }); 132 | 133 | return null; 134 | } 135 | }; 136 | -------------------------------------------------------------------------------- /commands/item/trade.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | 3 | const Currency = require('../../structures/currency/Currency'); 4 | const Inventory = require('../../structures/currency/Inventory'); 5 | const ItemGroup = require('../../structures/currency/ItemGroup'); 6 | 7 | module.exports = class ItemTradeCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'item-trade', 11 | aliases: ['trade-items', 'trade-item', 'items-trade'], 12 | group: 'item', 13 | memberName: 'trade', 14 | description: `Trade items with another user.`, 15 | guildOnly: true, 16 | throttling: { 17 | usages: 2, 18 | duration: 3 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'member', 24 | prompt: 'what user would you like to give your item(s)?\n', 25 | type: 'member' 26 | }, 27 | { 28 | key: 'offerItem', 29 | prompt: 'what item would you like to trade?\n', 30 | type: 'string' 31 | }, 32 | { 33 | key: 'offerAmount', 34 | label: 'amount of items to give', 35 | prompt: 'how many would you like to trade?\n', 36 | type: 'integer', 37 | min: 1 38 | }, 39 | { 40 | key: 'receiveItem', 41 | prompt: 'what item would you like to receive?\n', 42 | type: 'string' 43 | }, 44 | { 45 | key: 'receiveAmount', 46 | label: 'amount of items to receive', 47 | prompt: 'how many items would you like to receive?\n', 48 | type: 'integer', 49 | min: 1 50 | } 51 | ] 52 | }); 53 | } 54 | 55 | async run(msg, args) { 56 | const { member, offerAmount, receiveAmount } = args; 57 | const offerItem = this.isDonuts(args.offerItem, offerAmount); 58 | const receiveItem = this.isDonuts(args.receiveItem, receiveAmount); 59 | 60 | if (member.id === msg.author.id) return msg.reply('what are you trying to achieve by trading with yourself?'); 61 | if (member.user.bot) return msg.reply('bots got nothing to trade, man.'); 62 | if (!offerItem && !receiveItem) return msg.reply("you can't trade donuts for donuts."); 63 | 64 | const offerBalance = Currency.getBalance(msg.author.id); 65 | const receiveBalance = Currency.getBalance(member.id); 66 | const offerInv = Inventory.fetchInventory(msg.author.id); 67 | const receiveInv = Inventory.fetchInventory(member.id); 68 | const offerItemBalance = offerInv.content[offerItem] ? offerInv.content[offerItem].amount : null; 69 | const receiveItemBalance = receiveInv.content[receiveItem] ? receiveInv.content[receiveItem].amount : null; 70 | const offerValidation = this.validate(offerItem, offerAmount, offerBalance, offerItemBalance, 'you'); 71 | const receiveValidation = this.validate(receiveItem, receiveAmount, receiveBalance, receiveItemBalance, member.displayName); // eslint-disable-line max-len 72 | if (offerValidation) return msg.reply(offerValidation); 73 | if (receiveValidation) return msg.reply(receiveValidation); 74 | 75 | const embed = { 76 | title: `${msg.member.displayName} < -- > ${member.displayName}`, 77 | description: 'Type `accept` within the next 30 seconds to accept this trade.', 78 | fields: [ 79 | { 80 | name: msg.member.displayName, 81 | value: offerItem 82 | ? `${offerAmount} ${offerItem}` 83 | : Currency.convert(offerAmount) 84 | }, 85 | { 86 | name: member.displayName, 87 | value: receiveItem 88 | ? `${receiveAmount} ${receiveItem}` 89 | : Currency.convert(receiveAmount) 90 | } 91 | ] 92 | }; 93 | 94 | if (!await this.response(msg, member, embed)) return msg.reply(`${member.displayName} declined or failed to respond.`); // eslint-disable-line 95 | if (!offerItem) this.sendDonuts(msg.author, member, offerAmount); 96 | else this.sendItems(offerInv, receiveInv, offerItem, offerAmount); 97 | if (!receiveItem) this.sendDonuts(member, msg.author, receiveAmount); 98 | else this.sendItems(receiveInv, offerInv, receiveItem, receiveAmount); 99 | return msg.say('Trade successful.'); 100 | } 101 | 102 | validate(item, amount, balance, itemBalance, user) { 103 | const person = user === 'you'; 104 | if (item) { 105 | if (!itemBalance) return `${user} ${person ? "don't" : "doesn't"} have any ${item}s.`; 106 | if (amount > itemBalance) return `${user} ${person ? 'have' : 'has'} only ${itemBalance} ${item}s.`; 107 | } else if (amount > balance) { 108 | return `${user} ${person ? 'have' : 'has'} only ${Currency.convert(balance)}`; 109 | } 110 | 111 | return null; 112 | } 113 | 114 | isDonuts(item, amount) { 115 | if (/donuts?/.test(item)) return null; 116 | 117 | return ItemGroup.convert(item, amount); 118 | } 119 | 120 | sendItems(fromInventory, toInventory, item, amount) { 121 | const itemGroup = new ItemGroup(item, amount); 122 | fromInventory.removeItems(itemGroup); 123 | toInventory.addItems(itemGroup); 124 | } 125 | 126 | sendDonuts(fromUser, toUser, amount) { 127 | Currency.removeBalance(fromUser, amount); 128 | Currency.addBalance(toUser, amount); 129 | } 130 | 131 | async response(msg, user, embed) { 132 | msg.say(`${user}, ${msg.member.displayName} wants to trade with you.`); 133 | msg.embed(embed); 134 | const responses = await msg.channel.awaitMessages(response => 135 | response.author.id === user.id && response.content.toLowerCase() === 'accept', 136 | { 137 | maxMatches: 1, 138 | time: 30e3 139 | }); 140 | 141 | if (responses.size === 0) return false; 142 | return true; 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /Commando.js: -------------------------------------------------------------------------------- 1 | const { FriendlyError } = require('discord.js-commando'); 2 | const { oneLine } = require('common-tags'); 3 | const path = require('path'); 4 | const winston = require('winston'); 5 | 6 | const SequelizeProvider = require('./providers/Sequelize'); 7 | const { OWNERS, COMMAND_PREFIX, TOKEN } = process.env; 8 | 9 | const CommandoClient = require('./structures/CommandoClient'); 10 | const client = new CommandoClient({ 11 | owner: OWNERS.split(','), 12 | commandPrefix: COMMAND_PREFIX, 13 | unknownCommandResponse: false, 14 | disableEveryone: true 15 | }); 16 | 17 | const Currency = require('./structures/currency/Currency'); 18 | const Experience = require('./structures/currency/Experience'); 19 | const userName = require('./models/UserName'); 20 | 21 | let earnedRecently = []; 22 | let gainedXPRecently = []; 23 | 24 | client.setProvider(new SequelizeProvider(client.database)); 25 | 26 | client.dispatcher.addInhibitor(msg => { 27 | const blacklist = client.provider.get('global', 'userBlacklist', []); 28 | if (!blacklist.includes(msg.author.id)) return false; 29 | return `Has been blacklisted.`; 30 | }); 31 | 32 | client.on('error', winston.error) 33 | .on('warn', winston.warn) 34 | .once('ready', () => Currency.leaderboard()) 35 | .on('ready', () => { 36 | winston.info(oneLine` 37 | [DISCORD]: Client ready... 38 | Logged in as ${client.user.tag} (${client.user.id}) 39 | `); 40 | }) 41 | .on('disconnect', () => winston.warn('[DISCORD]: Disconnected!')) 42 | .on('reconnect', () => winston.warn('[DISCORD]: Reconnecting...')) 43 | .on('commandRun', (cmd, promise, msg, args) => 44 | winston.info(oneLine` 45 | [DISCORD]: ${msg.author.tag} (${msg.author.id}) 46 | > ${msg.guild ? `${msg.guild.name} (${msg.guild.id})` : 'DM'} 47 | >> ${cmd.groupID}:${cmd.memberName} 48 | ${Object.values(args).length ? `>>> ${Object.values(args)}` : ''} 49 | `) 50 | ) 51 | .on('unknownCommand', msg => { 52 | if (msg.channel.type === 'dm') return; 53 | if (msg.author.bot) return; 54 | if (msg.content.split(msg.guild.commandPrefix)[1] === 'undefined') return; 55 | const args = { name: msg.content.split(msg.guild.commandPrefix)[1].toLowerCase() }; 56 | client.registry.resolveCommand('tags:tag').run(msg, args); 57 | }) 58 | .on('message', async message => { 59 | if (message.channel.type === 'dm') return; 60 | if (message.author.bot) return; 61 | 62 | const channelLocks = client.provider.get(message.guild.id, 'locks', []); 63 | if (channelLocks.includes(message.channel.id)) return; 64 | if (!earnedRecently.includes(message.author.id)) { 65 | const hasImageAttachment = message.attachments.some(attachment => 66 | attachment.url.match(/\.(png|jpg|jpeg|gif|webp)$/) 67 | ); 68 | const moneyEarned = hasImageAttachment 69 | ? Math.ceil(Math.random() * 7) + 5 70 | : Math.ceil(Math.random() * 7) + 1; 71 | 72 | Currency._changeBalance(message.author.id, moneyEarned); 73 | 74 | earnedRecently.push(message.author.id); 75 | setTimeout(() => { 76 | const index = earnedRecently.indexOf(message.author.id); 77 | earnedRecently.splice(index, 1); 78 | }, 8000); 79 | } 80 | 81 | if (!gainedXPRecently.includes(message.author.id)) { 82 | const xpEarned = Math.ceil(Math.random() * 9) + 3; 83 | const oldLevel = await Experience.getLevel(message.author.id); 84 | 85 | Experience.addExperience(message.author.id, xpEarned).then(async () => { 86 | const newLevel = await Experience.getLevel(message.author.id); 87 | if (newLevel > oldLevel) { 88 | Currency._changeBalance(message.author.id, 100 * newLevel); 89 | } 90 | }).catch(err => null); // eslint-disable-line no-unused-vars, handle-callback-err 91 | 92 | gainedXPRecently.push(message.author.id); 93 | setTimeout(() => { 94 | const index = gainedXPRecently.indexOf(message.author.id); 95 | gainedXPRecently.splice(index, 1); 96 | }, 60 * 1000); 97 | } 98 | }) 99 | .on('commandError', (cmd, err) => { 100 | if (err instanceof FriendlyError) return; 101 | winston.error(`[DISCORD]: Error in command ${cmd.groupID}:${cmd.memberName}`, err); 102 | }) 103 | .on('commandBlocked', (msg, reason) => { 104 | winston.info(oneLine` 105 | [DISCORD]: Command ${msg.command ? `${msg.command.groupID}:${msg.command.memberName}` : ''} 106 | blocked; User ${msg.author.tag} (${msg.author.id}): ${reason} 107 | `); 108 | }) 109 | .on('commandPrefixChange', (guild, prefix) => { 110 | winston.info(oneLine` 111 | [DISCORD]: Prefix changed to ${prefix || 'the default'} 112 | ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. 113 | `); 114 | }) 115 | .on('commandStatusChange', (guild, command, enabled) => { 116 | winston.info(oneLine` 117 | [DISCORD]: Command ${command.groupID}:${command.memberName} 118 | ${enabled ? 'enabled' : 'disabled'} 119 | ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. 120 | `); 121 | }) 122 | .on('groupStatusChange', (guild, group, enabled) => { 123 | winston.info(oneLine` 124 | [DISCORD]: Group ${group.id} 125 | ${enabled ? 'enabled' : 'disabled'} 126 | ${guild ? `in guild ${guild.name} (${guild.id})` : 'globally'}. 127 | `); 128 | }) 129 | .on('userUpdate', (oldUser, newUser) => { 130 | if (oldUser.username !== newUser.username) { 131 | userName.create({ userID: newUser.id, username: oldUser.username }).catch(err => null); // eslint-disable-line no-unused-vars, handle-callback-err, max-len 132 | } 133 | }); 134 | 135 | client.registry 136 | .registerGroups([ 137 | ['info', 'Info'], 138 | ['economy', 'Economy'], 139 | ['social', 'Social'], 140 | ['games', 'Games'], 141 | ['item', 'Item'], 142 | ['weather', 'Weather'], 143 | ['music', 'Music'], 144 | ['tags', 'Tags'], 145 | ['docs', 'Documentation'] 146 | ]) 147 | .registerDefaults() 148 | .registerTypesIn(path.join(__dirname, 'types')) 149 | .registerCommandsIn(path.join(__dirname, 'commands')); 150 | 151 | client.login(TOKEN); 152 | -------------------------------------------------------------------------------- /commands/social/profile.js: -------------------------------------------------------------------------------- 1 | const Canvas = require('canvas'); 2 | const { Command } = require('discord.js-commando'); 3 | 4 | const path = require('path'); 5 | const request = require('request-promise'); 6 | 7 | const { promisifyAll } = require('tsubaki'); 8 | const fs = promisifyAll(require('fs')); 9 | 10 | const Bank = require('../../structures/currency/Bank'); 11 | const Currency = require('../../structures/currency/Currency'); 12 | const Experience = require('../../structures/currency/Experience'); 13 | const UserProfile = require('../../models/UserProfile'); 14 | 15 | module.exports = class ProfileCommand extends Command { 16 | constructor(client) { 17 | super(client, { 18 | name: 'profile', 19 | aliases: ['p'], 20 | group: 'social', 21 | memberName: 'profile', 22 | description: 'Display your profile.', 23 | guildOnly: true, 24 | throttling: { 25 | usages: 1, 26 | duration: 60 27 | }, 28 | 29 | args: [ 30 | { 31 | key: 'member', 32 | prompt: 'whose profile would you like to view?\n', 33 | type: 'member', 34 | default: '' 35 | } 36 | ] 37 | }); 38 | } 39 | 40 | async run(msg, args) { 41 | const user = args.member || msg.member; 42 | const { Image } = Canvas; 43 | const profile = await UserProfile.findOne({ where: { userID: user.id } }); 44 | const personalMessage = profile ? profile.personalMessage : ''; 45 | const money = await Currency.getBalance(user.id); 46 | const balance = await Bank.getBalance(user.id); 47 | const networth = money + balance; 48 | const currentExp = await Experience.getCurrentExperience(user.id); 49 | const level = await Experience.getLevel(user.id); 50 | const levelBounds = await Experience.getLevelBounds(level); 51 | const totalExp = await Experience.getTotalExperience(user.id); 52 | const fillValue = Math.min(Math.max(currentExp / (levelBounds.upperBound - levelBounds.lowerBound), 0), 1); 53 | 54 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'profile', 'fonts', 'Roboto.ttf'), { family: 'Roboto' }); // eslint-disable-line max-len 55 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'profile', 'fonts', 'NotoEmoji-Regular.ttf'), { family: 'Roboto' }); // eslint-disable-line max-len 56 | 57 | const canvas = new Canvas(300, 300); 58 | const ctx = canvas.getContext('2d'); 59 | const lines = await this._wrapText(ctx, personalMessage, 110); 60 | const base = new Image(); 61 | const cond = new Image(); 62 | const generate = () => { 63 | // Environment Variables 64 | ctx.drawImage(base, 0, 0); 65 | ctx.scale(1, 1); 66 | ctx.patternQuality = 'billinear'; 67 | ctx.filter = 'bilinear'; 68 | ctx.antialias = 'subpixel'; 69 | ctx.shadowColor = 'rgba(0, 0, 0, 0.4)'; 70 | ctx.shadowOffsetY = 2; 71 | ctx.shadowBlur = 2; 72 | 73 | // Username 74 | ctx.font = '20px Roboto'; 75 | ctx.fillStyle = '#FFFFFF'; 76 | ctx.fillText(user.displayName, 50, 173); 77 | 78 | // EXP 79 | ctx.font = '10px Roboto'; 80 | ctx.textAlign = 'center'; 81 | ctx.fillStyle = '#3498DB'; 82 | ctx.shadowColor = 'rgba(0, 0, 0, 0)'; 83 | ctx.fillRect(10, 191, fillValue * 135, 17); 84 | 85 | // EXP 86 | ctx.font = '10px Roboto'; 87 | ctx.textAlign = 'center'; 88 | ctx.fillStyle = '#333333'; 89 | ctx.shadowColor = 'rgba(0, 0, 0, 0)'; 90 | ctx.fillText(`EXP: ${currentExp}/${levelBounds.upperBound - levelBounds.lowerBound}`, 78, 203); 91 | 92 | // LVL 93 | ctx.font = '30px Roboto'; 94 | ctx.textAlign = 'left'; 95 | ctx.fillStyle = '#E5E5E5'; 96 | ctx.shadowColor = 'rgba(0, 0, 0, 0.4)'; 97 | ctx.fillText('LVL.', 12, 235); 98 | 99 | // LVL Number 100 | ctx.font = '30px Roboto'; 101 | ctx.fillStyle = '#E5E5E5'; 102 | ctx.fillText(level, 86, 235); 103 | 104 | // Total EXP 105 | ctx.font = '14px Roboto'; 106 | ctx.fillStyle = '#E5E5E5'; 107 | ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'; 108 | ctx.fillText('Total EXP', 12, 254); 109 | 110 | // Total EXP Number 111 | ctx.font = '14px Roboto'; 112 | ctx.fillStyle = '#E5E5E5'; 113 | ctx.fillText(totalExp, 86, 254); 114 | 115 | /* // Global Rank 116 | ctx.font = '14px Roboto'; 117 | ctx.fillStyle = '#E5E5E5'; 118 | ctx.fillText('Rank', 12, 270); 119 | 120 | // Global Rank Number 121 | ctx.font = '14px Roboto'; 122 | ctx.fillStyle = '#E5E5E5'; 123 | ctx.fillText('#1', 86, 270); */ 124 | 125 | // Currency 126 | ctx.font = '14px Roboto'; 127 | ctx.fillStyle = '#E5E5E5'; 128 | ctx.fillText('Net Worth', 12, 287); 129 | 130 | // Currency Number 131 | ctx.font = '14px Roboto'; 132 | ctx.fillStyle = '#E5E5E5'; 133 | ctx.fillText(networth, 86, 287); 134 | 135 | // Info title 136 | ctx.font = '12px Roboto'; 137 | ctx.fillStyle = '#333333'; 138 | ctx.shadowColor = 'rgba(0, 0, 0, 0)'; 139 | ctx.fillText('Info Box', 182, 207); 140 | 141 | // Info 142 | ctx.font = '12px Roboto'; 143 | ctx.fillStyle = '#333333'; 144 | lines.forEach((line, i) => { 145 | ctx.fillText(line, 162, (i + 18.6) * parseInt(12, 0)); 146 | }); 147 | 148 | // Image 149 | ctx.beginPath(); 150 | ctx.arc(79, 76, 55, 0, Math.PI * 2, true); 151 | ctx.closePath(); 152 | ctx.clip(); 153 | ctx.shadowBlur = 5; 154 | ctx.shadowColor = 'rgba(0, 0, 0, 0.2)'; 155 | ctx.drawImage(cond, 24, 21, 110, 110); 156 | }; 157 | base.src = await fs.readFileAsync(path.join(__dirname, '..', '..', 'assets', 'profile', 'backgrounds', `${profile ? profile.background : 'default'}.png`)); // eslint-disable-line max-len 158 | cond.src = await request({ 159 | uri: user.user.displayAvatarURL({ format: 'png' }), 160 | encoding: null 161 | }); 162 | generate(); 163 | 164 | return msg.channel.send({ files: [{ attachment: canvas.toBuffer(), name: 'profile.png' }] }); 165 | } 166 | 167 | _wrapText(ctx, text, maxWidth) { 168 | return new Promise(resolve => { 169 | const words = text.split(' '); 170 | let lines = []; 171 | let line = ''; 172 | 173 | if (ctx.measureText(text).width < maxWidth) { 174 | return resolve([text]); 175 | } 176 | 177 | while (words.length > 0) { 178 | let split = false; 179 | while (ctx.measureText(words[0]).width >= maxWidth) { 180 | const tmp = words[0]; 181 | words[0] = tmp.slice(0, -1); 182 | 183 | if (!split) { 184 | split = true; 185 | words.splice(1, 0, tmp.slice(-1)); 186 | } else { 187 | words[1] = tmp.slice(-1) + words[1]; 188 | } 189 | } 190 | 191 | if (ctx.measureText(line + words[0]).width < maxWidth) { 192 | line += `${words.shift()} `; 193 | } else { 194 | lines.push(line); 195 | line = ''; 196 | } 197 | 198 | if (words.length === 0) { 199 | lines.push(line); 200 | } 201 | } 202 | 203 | return resolve(lines); 204 | }); 205 | } 206 | }; 207 | -------------------------------------------------------------------------------- /commands/weather/weather.js: -------------------------------------------------------------------------------- 1 | // Credit goes to that cutie ;//w//; https://github.com/kurisubrooks/midori 2 | 3 | const Canvas = require('canvas'); 4 | const { Command } = require('discord.js-commando'); 5 | const path = require('path'); 6 | const request = require('request-promise'); 7 | const { promisifyAll } = require('tsubaki'); 8 | 9 | const fs = promisifyAll(require('fs')); 10 | 11 | const { GOOGLE_API, WEATHER_API } = process.env; 12 | const { version } = require('../../package'); 13 | 14 | module.exports = class WeatherCommand extends Command { 15 | constructor(client) { 16 | super(client, { 17 | name: 'weather', 18 | aliases: ['w', '☁', '⛅', '⛈', '🌤', '🌥', '🌦', '🌧', '🌨', '🌩', '🌪'], 19 | group: 'weather', 20 | memberName: 'weather', 21 | description: 'Get the weather.', 22 | throttling: { 23 | usages: 1, 24 | duration: 30 25 | }, 26 | 27 | args: [ 28 | { 29 | key: 'location', 30 | prompt: 'what location would you like to have information on?\n', 31 | type: 'string' 32 | } 33 | ] 34 | }); 35 | } 36 | 37 | async run(msg, args) { 38 | const { location } = args; 39 | const { Image } = Canvas; 40 | 41 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'weather', 'fonts', 'Roboto-Regular.ttf'), { family: 'Roboto' }); // eslint-disable-line max-len 42 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'weather', 'fonts', 'RobotoCondensed-Regular.ttf'), { family: 'Roboto Condensed' }); // eslint-disable-line max-len 43 | Canvas.registerFont(path.join(__dirname, '..', '..', 'assets', 'weather', 'fonts', 'RobotoMono-Light.ttf'), { family: 'Roboto Mono' }); // eslint-disable-line max-len 44 | 45 | if (!GOOGLE_API) return msg.reply('my Commander has not set the Google API Key. Go yell at him.'); 46 | if (!WEATHER_API) return msg.reply('my Commander has not set the Weather API Key. Go yell at him.'); 47 | 48 | const locationURI = encodeURIComponent(location.replace(/ /g, '+')); 49 | const response = await request({ 50 | uri: `https://maps.googleapis.com/maps/api/geocode/json?address=${locationURI}&key=${GOOGLE_API}`, 51 | headers: { 'User-Agent': `Commando v${version} (https://github.com/WeebDev/Commando/)` }, 52 | json: true 53 | }); 54 | 55 | if (response.status !== 'OK') return msg.reply(this.handleNotOK(msg, response.status)); 56 | if (response.results.length === 0) return msg.reply('your request returned no results.'); 57 | 58 | const geocodelocation = response.results[0].formatted_address; 59 | const params = `${response.results[0].geometry.location.lat},${response.results[0].geometry.location.lng}`; 60 | 61 | const locality = response.results[0].address_components.find(loc => loc.types.includes('locality')); 62 | const governing = response.results[0].address_components.find(gov => gov.types.includes('administrative_area_level_1')); // eslint-disable-line max-len 63 | const country = response.results[0].address_components.find(cou => cou.types.includes('country')); 64 | const continent = response.results[0].address_components.find(con => con.types.includes('continent')); 65 | 66 | const city = locality || governing || country || continent || {}; 67 | const state = locality && governing ? governing : locality ? country : {}; 68 | 69 | const res = await request({ 70 | uri: `https://api.darksky.net/forecast/${WEATHER_API}/${params}?exclude=minutely,hourly,flags&units=auto`, 71 | headers: { 'User-Agent': `Commando v${version} (https://github.com/WeebDev/Commando/)` }, 72 | json: true 73 | }); 74 | 75 | const condition = res.currently.summary; 76 | const { icon } = res.currently; 77 | const chanceofrain = Math.round((res.currently.precipProbability * 100) / 5) * 5; 78 | const temperature = Math.round(res.currently.temperature); 79 | const humidity = Math.round(res.currently.humidity * 100); 80 | 81 | const canvas = new Canvas(400, 180); 82 | const ctx = canvas.getContext('2d'); 83 | const base = new Image(); 84 | const cond = new Image(); 85 | const humid = new Image(); 86 | const precip = new Image(); 87 | 88 | let theme = 'light'; 89 | let fontColor = '#FFFFFF'; 90 | if (icon === 'snow' || icon === 'sleet' || icon === 'fog') { 91 | theme = 'dark'; 92 | fontColor = '#444444'; 93 | } 94 | 95 | const generate = () => { 96 | // Environment Variables 97 | ctx.drawImage(base, 0, 0); 98 | ctx.scale(1, 1); 99 | ctx.patternQuality = 'billinear'; 100 | ctx.filter = 'bilinear'; 101 | ctx.antialias = 'subpixel'; 102 | 103 | // City Name 104 | ctx.font = '20px Roboto'; 105 | ctx.fillStyle = fontColor; 106 | ctx.fillText(city.long_name ? city.long_name : 'Unknown', 35, 50); 107 | 108 | // Prefecture Name 109 | ctx.font = '16px Roboto'; 110 | ctx.fillStyle = theme === 'light' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)'; 111 | ctx.fillText(state.long_name ? state.long_name : '', 35, 72.5); 112 | 113 | // Temperature 114 | ctx.font = "48px 'Roboto Mono'"; 115 | ctx.fillStyle = fontColor; 116 | ctx.fillText(`${temperature}°`, 35, 140); 117 | 118 | // Condition 119 | ctx.font = '16px Roboto'; 120 | ctx.textAlign = 'right'; 121 | ctx.fillText(condition, 370, 142); 122 | 123 | // Condition Image 124 | ctx.drawImage(cond, 325, 31); 125 | 126 | // Humidity Image 127 | ctx.drawImage(humid, 358, 88); 128 | 129 | // Precip Image 130 | ctx.drawImage(precip, 358, 108); 131 | 132 | // Titles 133 | ctx.font = "16px 'Roboto Condensed'"; 134 | ctx.fillText(`${humidity}%`, 353, 100); 135 | ctx.fillText(`${chanceofrain}%`, 353, 121); 136 | }; 137 | 138 | base.src = await fs.readFileAsync(this.getBase(icon)); 139 | cond.src = await fs.readFileAsync(path.join(__dirname, '..', '..', 'assets', 'weather', 'icons', theme, `${icon}.png`)); // eslint-disable-line max-len 140 | humid.src = await fs.readFileAsync(path.join(__dirname, '..', '..', 'assets', 'weather', 'icons', theme, 'humidity.png')); // eslint-disable-line max-len 141 | precip.src = await fs.readFileAsync(path.join(__dirname, '..', '..', 'assets', 'weather', 'icons', theme, 'precip.png')); // eslint-disable-line max-len 142 | generate(); 143 | 144 | return msg.channel.send({ files: [{ attachment: canvas.toBuffer(), name: `${geocodelocation}.png` }] }); 145 | } 146 | 147 | handleNotOK(msg, status) { 148 | if (status === 'ZERO_RESULTS') return 'your request returned no results.'; 149 | else if (status === 'REQUEST_DENIED') return 'Geocode API Request was denied.'; 150 | else if (status === 'INVALID_REQUEST') return 'Invalid Request,'; 151 | else if (status === 'OVER_QUERY_LIMIT') return 'Query Limit Exceeded. Try again tomorrow.'; 152 | else return 'Unknown.'; 153 | } 154 | 155 | getBase(icon) { 156 | if (icon === 'clear-day' || icon === 'partly-cloudy-day') { 157 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'day.png'); 158 | } else if (icon === 'clear-night' || icon === 'partly-cloudy-night') { 159 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'night.png'); 160 | } else if (icon === 'rain') { 161 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'rain.png'); 162 | } else if (icon === 'thunderstorm') { 163 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'thunderstorm.png'); 164 | } else if (icon === 'snow' || icon === 'sleet' || icon === 'fog') { 165 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'snow.png'); 166 | } else if (icon === 'wind' || icon === 'tornado') { 167 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'windy.png'); 168 | } else if (icon === 'cloudy') { 169 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'cloudy.png'); 170 | } else { 171 | return path.join(__dirname, '..', '..', 'assets', 'weather', 'base', 'cloudy.png'); 172 | } 173 | } 174 | }; 175 | -------------------------------------------------------------------------------- /providers/Sequelize.js: -------------------------------------------------------------------------------- 1 | const { SettingProvider } = require('discord.js-commando'); 2 | const Sequelize = require('sequelize'); 3 | 4 | /** 5 | * Uses an PostgreSQL database to store settings with guilds 6 | * @extends {SettingProvider} 7 | */ 8 | class SequelizeProvider extends SettingProvider { 9 | /** 10 | * @external PostgreSQLDatabase 11 | * @see {@link https://www.npmjs.com/package/sequelize} 12 | */ 13 | 14 | /** 15 | * @param {SQLDatabase} db - Database for the provider 16 | */ 17 | constructor(db) { 18 | super(); 19 | 20 | /** 21 | * Database that will be used for storing/retrieving settings 22 | * @type {SQLDatabase} 23 | */ 24 | this.db = db; 25 | 26 | /** 27 | * Client that the provider is for (set once the client is ready, after using {@link CommandoClient#setProvider}) 28 | * @name SequelizeProvider#client 29 | * @type {CommandoClient} 30 | * @readonly 31 | */ 32 | Object.defineProperty(this, 'client', { value: null, writable: true }); 33 | 34 | /** 35 | * Settings cached in memory, mapped by guild ID (or 'global') 36 | * @type {Map} 37 | * @private 38 | */ 39 | this.settings = new Map(); 40 | 41 | /** 42 | * Listeners on the Client, mapped by the event name 43 | * @type {Map} 44 | * @private 45 | */ 46 | this.listeners = new Map(); 47 | 48 | /** 49 | * Sequelize Model Object 50 | * @type {SequelizeModel} 51 | * @private 52 | */ 53 | this.model = this.db.define('settings', { 54 | guild: { 55 | type: Sequelize.STRING, 56 | allowNull: false, 57 | unique: true, 58 | primaryKey: true 59 | }, 60 | settings: { type: Sequelize.TEXT } 61 | }); 62 | 63 | /** 64 | * @external SequelizeModel 65 | * @see {@link http://docs.sequelizejs.com/en/latest/api/model/} 66 | */ 67 | } 68 | 69 | async init(client) { 70 | this.client = client; 71 | await this.db.sync(); 72 | 73 | // Load all settings 74 | const rows = await this.model.findAll(); 75 | for (const row of rows) { 76 | let settings; 77 | try { 78 | settings = JSON.parse(row.dataValues.settings); 79 | } catch (err) { 80 | client.emit('warn', `SequelizeProvider couldn't parse the settings stored for guild ${row.dataValues.guild}.`); 81 | continue; 82 | } 83 | 84 | const guild = row.dataValues.guild !== '0' ? row.dataValues.guild : 'global'; 85 | 86 | this.settings.set(guild, settings); 87 | if (guild !== 'global' && !client.guilds.has(row.dataValues.guild)) continue; 88 | this.setupGuild(guild, settings); 89 | } 90 | 91 | // Listen for changes 92 | this.listeners 93 | .set('commandPrefixChange', (guild, prefix) => this.set(guild, 'prefix', prefix)) 94 | .set('commandStatusChange', (guild, command, enabled) => this.set(guild, `cmd-${command.name}`, enabled)) 95 | .set('groupStatusChange', (guild, group, enabled) => this.set(guild, `grp-${group.id}`, enabled)) 96 | .set('guildCreate', guild => { 97 | const settings = this.settings.get(guild.id); 98 | if (!settings) return; 99 | this.setupGuild(guild.id, settings); 100 | }) 101 | .set('commandRegister', command => { 102 | for (const [guild, settings] of this.settings) { 103 | if (guild !== 'global' && !client.guilds.has(guild)) continue; 104 | this.setupGuildCommand(client.guilds.get(guild), command, settings); 105 | } 106 | }) 107 | .set('groupRegister', group => { 108 | for (const [guild, settings] of this.settings) { 109 | if (guild !== 'global' && !client.guilds.has(guild)) continue; 110 | this.setupGuildGroup(client.guilds.get(guild), group, settings); 111 | } 112 | }); 113 | for (const [event, listener] of this.listeners) client.on(event, listener); 114 | } 115 | 116 | destroy() { 117 | // Remove all listeners from the client 118 | for (const [event, listener] of this.listeners) this.client.removeListener(event, listener); 119 | this.listeners.clear(); 120 | } 121 | 122 | get(guild, key, defVal) { 123 | const settings = this.settings.get(this.constructor.getGuildID(guild)); 124 | return settings ? typeof settings[key] !== 'undefined' ? settings[key] : defVal : defVal; 125 | } 126 | 127 | async set(guild, key, val) { 128 | guild = this.constructor.getGuildID(guild); 129 | let settings = this.settings.get(guild); 130 | if (!settings) { 131 | settings = {}; 132 | this.settings.set(guild, settings); 133 | } 134 | 135 | settings[key] = val; 136 | await this.model.upsert( 137 | { guild: guild !== 'global' ? guild : '0', settings: JSON.stringify(settings) }, 138 | { where: { guild: guild !== 'global' ? guild : '0' } } 139 | ); 140 | if (guild === 'global') this.updateOtherShards(key, val); 141 | return val; 142 | } 143 | 144 | async remove(guild, key) { 145 | guild = this.constructor.getGuildID(guild); 146 | const settings = this.settings.get(guild); 147 | if (!settings || typeof settings[key] === 'undefined') return undefined; 148 | 149 | const val = settings[key]; 150 | settings[key] = undefined; 151 | await this.model.upsert( 152 | { guild: guild !== 'global' ? guild : '0', settings: JSON.stringify(settings) }, 153 | { where: { guild: guild !== 'global' ? guild : '0' } } 154 | ); 155 | if (guild === 'global') this.updateOtherShards(key, undefined); 156 | return val; 157 | } 158 | 159 | async clear(guild) { 160 | guild = this.constructor.getGuildID(guild); 161 | if (!this.settings.has(guild)) return; 162 | this.settings.delete(guild); 163 | await this.model.destroy({ where: { guild: guild !== 'global' ? guild : '0' } }); 164 | } 165 | 166 | /** 167 | * Loads all settings for a guild 168 | * @param {string} guild - Guild ID to load the settings of (or 'global') 169 | * @param {Object} settings - Settings to load 170 | * @private 171 | */ 172 | setupGuild(guild, settings) { 173 | if (typeof guild !== 'string') throw new TypeError('The guild must be a guild ID or "global".'); 174 | guild = this.client.guilds.get(guild) || null; 175 | 176 | // Load the command prefix 177 | if (typeof settings.prefix !== 'undefined') { 178 | if (guild) guild._commandPrefix = settings.prefix; 179 | else this.client._commandPrefix = settings.prefix; 180 | } 181 | 182 | // Load all command/group statuses 183 | for (const command of this.client.registry.commands.values()) this.setupGuildCommand(guild, command, settings); 184 | for (const group of this.client.registry.groups.values()) this.setupGuildGroup(guild, group, settings); 185 | } 186 | 187 | /** 188 | * Sets up a command's status in a guild from the guild's settings 189 | * @param {?Guild} guild - Guild to set the status in 190 | * @param {Command} command - Command to set the status of 191 | * @param {Object} settings - Settings of the guild 192 | * @private 193 | */ 194 | setupGuildCommand(guild, command, settings) { 195 | if (typeof settings[`cmd-${command.name}`] === 'undefined') return; 196 | if (guild) { 197 | if (!guild._commandsEnabled) guild._commandsEnabled = {}; 198 | guild._commandsEnabled[command.name] = settings[`cmd-${command.name}`]; 199 | } else { 200 | command._globalEnabled = settings[`cmd-${command.name}`]; 201 | } 202 | } 203 | 204 | /** 205 | * Sets up a group's status in a guild from the guild's settings 206 | * @param {?Guild} guild - Guild to set the status in 207 | * @param {CommandGroup} group - Group to set the status of 208 | * @param {Object} settings - Settings of the guild 209 | * @private 210 | */ 211 | setupGuildGroup(guild, group, settings) { 212 | if (typeof settings[`grp-${group.id}`] === 'undefined') return; 213 | if (guild) { 214 | if (!guild._groupsEnabled) guild._groupsEnabled = {}; 215 | guild._groupsEnabled[group.id] = settings[`grp-${group.id}`]; 216 | } else { 217 | group._globalEnabled = settings[`grp-${group.id}`]; 218 | } 219 | } 220 | 221 | /** 222 | * Updates a global setting on all other shards if using the {@link ShardingManager}. 223 | * @param {string} key - Key of the setting to update 224 | * @param {*} val - Value of the setting 225 | * @private 226 | */ 227 | updateOtherShards(key, val) { 228 | if (!this.client.shard) return; 229 | key = JSON.stringify(key); 230 | val = typeof val !== 'undefined' ? JSON.stringify(val) : 'undefined'; 231 | this.client.shard.broadcastEval(` 232 | if(this.shard.id !== ${this.client.shard.id} && this.provider && this.provider.settings) { 233 | this.provider.settings.global[${key}] = ${val}; 234 | } 235 | `); 236 | } 237 | } 238 | 239 | module.exports = SequelizeProvider; 240 | -------------------------------------------------------------------------------- /commands/games/blackjack.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const { stripIndents } = require('common-tags'); 3 | 4 | const Blackjack = require('../../structures/games/Blackjack'); 5 | const Currency = require('../../structures/currency/Currency'); 6 | 7 | module.exports = class BlackjackCommand extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'blackjack', 11 | group: 'games', 12 | memberName: 'blackjack', 13 | description: `Play a game of blackjack for ${Currency.textPlural}!`, 14 | details: `Play a game of blackjack for ${Currency.textPlural}.`, 15 | guildOnly: true, 16 | throttling: { 17 | usages: 1, 18 | duration: 30 19 | }, 20 | 21 | args: [ 22 | { 23 | key: 'bet', 24 | prompt: `how many ${Currency.textPlural} do you want to bet?\n`, 25 | type: 'integer', 26 | validate: async (bet, msg) => { 27 | bet = parseInt(bet); 28 | const balance = await Currency.getBalance(msg.author.id); 29 | if (balance < bet) { 30 | return ` 31 | you don't have enough ${Currency.textPlural}. 32 | Your current account balance is ${Currency.convert(balance)}. 33 | Please specify a valid amount of ${Currency.textPlural}. 34 | `; 35 | } 36 | if (![100, 200, 300, 400, 500, 1000].includes(bet)) { 37 | return ` 38 | please choose \`100, 200, 300, 400, 500, 1000\` for your bet. 39 | `; 40 | } 41 | return true; 42 | } 43 | } 44 | ] 45 | }); 46 | } 47 | 48 | run(msg, { bet }) { 49 | if (Blackjack.gameExists(msg.author.id)) { 50 | return msg.reply(`you can't start 2 games of blackjack at the same time.`); 51 | } 52 | 53 | const blackjack = new Blackjack(msg); 54 | return msg.say( 55 | `New game of blackjack started with ${msg.member.displayName} with a bet of ${Currency.convert(bet)}!` 56 | ).then(async () => { 57 | const balance = await Currency.getBalance(msg.author.id); 58 | const playerHand = blackjack.getHand(); 59 | let dealerHand = blackjack.getHand(); 60 | let playerHands; 61 | 62 | if (Blackjack.handValue(playerHand) !== 'Blackjack') { 63 | playerHands = await this.getFinalHand(msg, playerHand, dealerHand, balance, bet, blackjack); 64 | const result = this.gameResult(Blackjack.handValue(playerHands[0]), 0); 65 | const noHit = playerHands.length === 1 && result === 'bust'; 66 | 67 | while ((Blackjack.isSoft(dealerHand) 68 | || Blackjack.handValue(dealerHand) < 17) 69 | && !noHit) { // eslint-disable-line no-unmodified-loop-condition 70 | blackjack.hit(dealerHand); 71 | } 72 | } else { 73 | playerHands = [playerHand]; 74 | } 75 | 76 | blackjack.endGame(); 77 | 78 | const dealerValue = Blackjack.handValue(dealerHand); 79 | let winnings = 0; 80 | let hideHoleCard = true; 81 | const embed = { 82 | title: `Blackjack | ${msg.member.displayName}`, 83 | fields: [], 84 | footer: { text: blackjack.cardsRemaining() ? `Cards remaining: ${blackjack.cardsRemaining()}` : `Shuffling` } 85 | }; 86 | 87 | playerHands.forEach((hand, i) => { 88 | const playerValue = Blackjack.handValue(hand); 89 | const result = this.gameResult(playerValue, dealerValue); 90 | 91 | if (result !== 'bust') hideHoleCard = false; 92 | 93 | const lossOrGain = Math.floor((['loss', 'bust'].includes(result) 94 | ? -1 : result === 'push' 95 | ? 0 : 1) * (hand.doubled 96 | ? 2 : 1) * (playerValue === 'Blackjack' 97 | ? 1.5 : 1) * bet); 98 | 99 | winnings += lossOrGain; 100 | const soft = Blackjack.isSoft(hand); 101 | /* eslint-disable max-len */ 102 | embed.fields.push({ 103 | name: playerHands.length === 1 ? '**Your hand**' : `**Hand ${i + 1}**`, 104 | value: stripIndents` 105 | ${hand.join(' - ')} 106 | Value: ${soft ? 'Soft ' : ''}${playerValue} 107 | 108 | Result: ${result.replace(/(^\w|\s\w)/g, ma => ma.toUpperCase())}${result !== 'push' ? `, ${Currency.convert(lossOrGain)}` : `, ${Currency.textPlural} back`} 109 | `, 110 | inline: true 111 | }); 112 | /* eslint-enable max-len */ 113 | }); 114 | 115 | embed.fields.push({ 116 | name: '\u200B', 117 | value: '\u200B' 118 | }); 119 | 120 | embed.fields.push({ 121 | name: '**Dealer hand**', 122 | value: stripIndents` 123 | ${hideHoleCard ? `${dealerHand[0]} - XX` : dealerHand.join(' - ')} 124 | Value: ${hideHoleCard ? Blackjack.handValue([dealerHand[0]]) : dealerValue} 125 | ` 126 | }); 127 | 128 | embed.color = winnings > 0 ? 0x009900 : winnings < 0 ? 0x990000 : undefined; 129 | embed.description = `You ${winnings === 0 130 | ? 'broke even' : `${winnings > 0 131 | ? 'won' : 'lost'} ${Currency.convert(Math.abs(winnings))}`}`; 132 | 133 | if (winnings !== 0) Currency.changeBalance(msg.author.id, winnings); 134 | 135 | return msg.embed(embed); 136 | }); 137 | } 138 | 139 | gameResult(playerValue, dealerValue) { 140 | if (playerValue > 21) return 'bust'; 141 | if (dealerValue > 21) return 'dealer bust'; 142 | if (playerValue === dealerValue) return 'push'; 143 | if (playerValue === 'Blackjack' || playerValue > dealerValue) return 'win'; 144 | 145 | return 'loss'; 146 | } 147 | 148 | getFinalHand(msg, playerHand, dealerHand, balance, bet, blackjack) { 149 | return new Promise(async resolve => { 150 | const hands = [playerHand]; 151 | let currentHand = hands[0]; 152 | let totalBet = bet; 153 | 154 | const nextHand = () => currentHand = hands[hands.indexOf(currentHand) + 1]; // eslint-disable-line no-return-assign, max-len 155 | while (currentHand) { // eslint-disable-line no-unmodified-loop-condition 156 | if (currentHand.length === 1) blackjack.hit(currentHand); 157 | if (Blackjack.handValue(currentHand) === 'Blackjack') { 158 | nextHand(); 159 | continue; 160 | } 161 | if (Blackjack.handValue(currentHand) >= 21) { 162 | nextHand(); 163 | continue; 164 | } 165 | if (currentHand.doubled) { 166 | blackjack.hit(currentHand); 167 | nextHand(); 168 | continue; 169 | } 170 | 171 | const canDoubleDown = balance >= totalBet + bet && currentHand.length === 2; 172 | const canSplit = balance >= totalBet + bet 173 | && Blackjack.handValue([currentHand[0]]) === Blackjack.handValue([currentHand[1]]) 174 | && currentHand.length === 2; 175 | 176 | await msg.embed({ // eslint-disable-line no-await-in-loop 177 | title: `Blackjack | ${msg.member.displayName}`, 178 | description: !canDoubleDown && !canSplit 179 | ? 'Type `hit` to draw another card or `stand` to pass.' 180 | : `Type \`hit\` to draw another card, ${canDoubleDown 181 | ? '`double down` to double down, ' 182 | : ''}${canSplit 183 | ? '`split` to split, ' : ''}or \`stand\` to pass.`, 184 | fields: [ 185 | { 186 | name: hands.length === 1 187 | ? '**Your hand**' 188 | : `**Hand ${hands.indexOf(currentHand) + 1}**`, 189 | value: stripIndents` 190 | ${currentHand.join(' - ')} 191 | Value: ${Blackjack.isSoft(currentHand) ? 'Soft ' : ''}${Blackjack.handValue(currentHand)} 192 | `, 193 | inline: true 194 | }, 195 | { 196 | name: '**Dealer hand**', 197 | value: stripIndents` 198 | ${dealerHand[0]} - XX 199 | Value: ${Blackjack.isSoft([dealerHand[0]]) ? 'Soft ' : ''}${Blackjack.handValue([dealerHand[0]])} 200 | `, 201 | inline: true 202 | } 203 | ], 204 | footer: { text: blackjack.cardsRemaining() ? `Cards remaining: ${blackjack.cardsRemaining()}` : `Shuffling` } 205 | }); 206 | 207 | const responses = await msg.channel.awaitMessages(msg2 => // eslint-disable-line no-await-in-loop 208 | msg2.author.id === msg.author.id && ( 209 | msg2.content === 'hit' 210 | || msg2.content === 'stand' 211 | || (msg2.content === 'split' && canSplit) 212 | || (msg2.content === 'double down' && canDoubleDown) 213 | ), { 214 | maxMatches: 1, 215 | time: 20e3 216 | }); 217 | 218 | if (responses.size === 0) break; 219 | const action = responses.first().content.toLowerCase(); 220 | if (action === 'stand' || Blackjack.handValue(currentHand) >= 21) { 221 | if (currentHand === hands[hands.length - 1]) break; 222 | nextHand(); 223 | } 224 | if (action === 'hit') blackjack.hit(currentHand); 225 | if (action === 'split' && canSplit) { 226 | totalBet += bet; 227 | hands.push([currentHand.pop()]); 228 | blackjack.hit(currentHand); 229 | } 230 | if (action === 'double down' && canDoubleDown) { 231 | totalBet += bet; 232 | currentHand.doubled = true; 233 | } 234 | } 235 | 236 | return resolve(hands); 237 | }); 238 | } 239 | }; 240 | -------------------------------------------------------------------------------- /commands/docs/docs.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('discord.js-commando'); 2 | const request = require('snekfetch'); 3 | const { oneLineTrim } = require('common-tags'); 4 | 5 | module.exports = class DocsCommand extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'docs', 9 | group: 'docs', 10 | memberName: 'docs', 11 | description: 'Searches discord.js documentation.', 12 | throttling: { 13 | usages: 2, 14 | duration: 3 15 | }, 16 | 17 | args: [ 18 | { 19 | key: 'query', 20 | prompt: 'what would you like to find?\n', 21 | type: 'string' 22 | }, 23 | { 24 | key: 'version', 25 | prompt: 'which version of docs would you like (stable, master, commando)?', 26 | type: 'string', 27 | parse: value => value.toLowerCase(), 28 | validate: value => ['master', 'stable', 'commando'].includes(value), 29 | default: 'stable' 30 | } 31 | ] 32 | }); 33 | 34 | // Cache for docs 35 | this.docs = {}; 36 | } 37 | 38 | async fetchDocs(version) { 39 | if (this.docs[version]) return this.docs[version]; 40 | 41 | const link = version === 'commando' 42 | ? 'https://raw.githubusercontent.com/Gawdl3y/discord.js-commando/docs/master.json' 43 | : `https://raw.githubusercontent.com/hydrabolt/discord.js/docs/${version}.json`; 44 | 45 | const { text } = await request.get(link); 46 | const json = JSON.parse(text); 47 | 48 | this.docs[version] = json; 49 | return json; 50 | } 51 | 52 | search(docs, query) { 53 | query = query.split(/[#.]/); 54 | const mainQuery = query[0].toLowerCase(); 55 | let memberQuery = query[1] ? query[1].toLowerCase() : null; 56 | 57 | const findWithin = (parentItem, props, name) => { 58 | let found = null; 59 | for (const category of props) { 60 | if (!parentItem[category]) continue; 61 | const item = parentItem[category].find(i => i.name.toLowerCase() === name); 62 | if (item) { 63 | found = { item, category }; 64 | break; 65 | } 66 | } 67 | 68 | return found; 69 | }; 70 | 71 | const main = findWithin(docs, ['classes', 'interfaces', 'typedefs'], mainQuery); 72 | if (!main) return []; 73 | 74 | const res = [main]; 75 | if (!memberQuery) return res; 76 | 77 | let props; 78 | if (/\(.*?\)$/.test(memberQuery)) { 79 | memberQuery = memberQuery.replace(/\(.*?\)$/, ''); 80 | props = ['methods']; 81 | } else { 82 | props = main.category === 'typedefs' ? ['props'] : ['props', 'methods', 'events']; 83 | } 84 | 85 | const member = findWithin(main.item, props, memberQuery); 86 | if (!member) return []; 87 | 88 | const rest = query.slice(2); 89 | if (rest.length) { 90 | if (!member.item.type) return []; 91 | const base = this.joinType(member.item.type) 92 | .replace(/<.+>/g, '') 93 | .replace(/\|.+/, '') 94 | .trim(); 95 | 96 | return this.search(docs, `${base}.${rest.join('.')}`); 97 | } 98 | 99 | res.push(member); 100 | return res; 101 | } 102 | 103 | clean(text) { 104 | return text.replace(/\n/g, ' ') 105 | .replace(/<\/?(?:info|warn)>/g, '') 106 | .replace(/\{@link (.+?)\}/g, '`$1`'); 107 | } 108 | 109 | joinType(type) { 110 | return type.map(t => t.map(a => Array.isArray(a) ? a.join('') : a).join('')).join(' | '); 111 | } 112 | 113 | getLink(version) { 114 | return version === 'commando' 115 | ? 'https://discord.js.org/#/docs/commando/master/' 116 | : `https://discord.js.org/#/docs/main/${version}/`; 117 | } 118 | 119 | makeLink(main, member, version) { 120 | return oneLineTrim` 121 | ${this.getLink(version)} 122 | ${main.category === 'classes' ? 'class' : 'typedef'}/${main.item.name} 123 | ?scrollTo=${member.item.scope === 'static' ? 's-' : ''}${member.item.name} 124 | `; 125 | } 126 | 127 | formatMain(main, version) { 128 | const embed = { 129 | description: `__**[${main.item.name}`, 130 | fields: [] 131 | }; 132 | 133 | if (main.item.extends) embed.description += ` (extends ${main.item.extends[0]})`; 134 | 135 | embed.description += oneLineTrim` 136 | ](${this.getLink(version)} 137 | ${main.category === 'classes' ? 'class' : 'typedef'}/${main.item.name})**__ 138 | `; 139 | 140 | embed.description += '\n'; 141 | if (main.item.description) embed.description += `\n${this.clean(main.item.description)}`; 142 | 143 | const join = it => `\`${it.map(i => i.name).join('` `')}\``; 144 | 145 | if (main.item.props) { 146 | embed.fields.push({ 147 | name: 'Properties', 148 | value: join(main.item.props) 149 | }); 150 | } 151 | 152 | if (main.item.methods) { 153 | embed.fields.push({ 154 | name: 'Methods', 155 | value: join(main.item.methods) 156 | }); 157 | } 158 | 159 | if (main.item.events) { 160 | embed.fields.push({ 161 | name: 'Events', 162 | value: join(main.item.events) 163 | }); 164 | } 165 | 166 | return embed; 167 | } 168 | 169 | formatProp(main, member, version) { 170 | const embed = { 171 | description: oneLineTrim` 172 | __**[${main.item.name}${member.item.scope === 'static' ? '.' : '#'}${member.item.name}] 173 | (${this.makeLink(main, member, version)})**__ 174 | `, 175 | fields: [] 176 | }; 177 | 178 | embed.description += '\n'; 179 | if (member.item.description) embed.description += `\n${this.clean(member.item.description)}`; 180 | 181 | const type = this.joinType(member.item.type); 182 | embed.fields.push({ 183 | name: 'Type', 184 | value: `\`${type}\`` 185 | }); 186 | 187 | if (member.item.examples) { 188 | embed.fields.push({ 189 | name: 'Example', 190 | value: `\`\`\`js\n${member.item.examples.join('```\n```js\n')}\`\`\`` 191 | }); 192 | } 193 | 194 | return embed; 195 | } 196 | 197 | formatMethod(main, member, version) { 198 | const embed = { 199 | description: oneLineTrim` 200 | __**[${main.item.name}${member.item.scope === 'static' ? '.' : '#'}${member.item.name}()] 201 | (${this.makeLink(main, member, version)})**__ 202 | `, 203 | fields: [] 204 | }; 205 | 206 | embed.description += '\n'; 207 | if (member.item.description) embed.description += `\n${this.clean(member.item.description)}`; 208 | 209 | if (member.item.params) { 210 | const params = member.item.params.map(param => { 211 | const name = param.optional ? `[${param.name}]` : param.name; 212 | const type = this.joinType(param.type); 213 | return `\`${name}: ${type}\`\n${this.clean(param.description)}`; 214 | }); 215 | 216 | embed.fields.push({ 217 | name: 'Parameters', 218 | value: params.join('\n\n') 219 | }); 220 | } 221 | 222 | if (member.item.returns) { 223 | const desc = member.item.returns.description ? `${this.clean(member.item.returns.description)}\n` : ''; 224 | const type = this.joinType(member.item.returns.types || member.item.returns); 225 | const returns = `${desc}\`=> ${type}\``; 226 | embed.fields.push({ 227 | name: 'Returns', 228 | value: returns 229 | }); 230 | } else { 231 | embed.fields.push({ 232 | name: 'Returns', 233 | value: '`=> void`' 234 | }); 235 | } 236 | 237 | if (member.item.examples) { 238 | embed.fields.push({ 239 | name: 'Example', 240 | value: `\`\`\`js\n${member.item.examples.join('```\n```js\n')}\`\`\`` 241 | }); 242 | } 243 | 244 | return embed; 245 | } 246 | 247 | formatEvent(main, member, version) { 248 | const embed = { 249 | description: `__**[${main.item.name}#${member.item.name}](${this.makeLink(main, member, version)})**__\n`, 250 | fields: [] 251 | }; 252 | 253 | if (member.item.description) embed.description += `\n${this.clean(member.item.description)}`; 254 | 255 | if (member.item.params) { 256 | const params = member.item.params.map(param => { 257 | const type = this.joinType(param.type); 258 | return `\`${param.name}: ${type}\`\n${this.clean(param.description)}`; 259 | }); 260 | 261 | embed.fields.push({ 262 | name: 'Parameters', 263 | value: params.join('\n\n') 264 | }); 265 | } 266 | 267 | if (member.item.examples) { 268 | embed.fields.push({ 269 | name: 'Example', 270 | value: `\`\`\`js\n${member.item.examples.join('```\n```js\n')}\`\`\`` 271 | }); 272 | } 273 | 274 | return embed; 275 | } 276 | 277 | async run(msg, { query, version }) { 278 | const docs = await this.fetchDocs(version); 279 | const [main, member] = this.search(docs, query); 280 | 281 | if (!main) { 282 | return msg.say('Could not find that item in the docs.'); 283 | } 284 | 285 | const embed = member ? { 286 | props: this.formatProp, 287 | methods: this.formatMethod, 288 | events: this.formatEvent 289 | }[member.category].call(this, main, member, version) : this.formatMain(main, version); 290 | 291 | const icon = 'https://cdn.discordapp.com/icons/222078108977594368/bc226f09db83b9176c64d923ff37010b.webp'; 292 | embed.url = this.getLink(version); 293 | embed.author = { 294 | name: version === 'commando' ? 'Commando Docs' : `Discord.js Docs (${version})`, 295 | icon_url: icon // eslint-disable-line camelcase 296 | }; 297 | 298 | return msg.embed(embed); 299 | } 300 | }; 301 | --------------------------------------------------------------------------------