Inital commit

This commit is contained in:
Max Isom 2020-03-09 11:57:39 -05:00
parent afadcb9ee5
commit eca84c8b69
14 changed files with 2884 additions and 0 deletions

5
.gitignore vendored
View file

@ -1,3 +1,8 @@
data
.DS_Store
dist
dts
# Logs
logs
*.log

6
nodemon.json Normal file
View file

@ -0,0 +1,6 @@
{
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
"watch": ["src"],
"exec": "npm start",
"ext": "ts"
}

73
package.json Normal file
View file

@ -0,0 +1,73 @@
{
"name": "muse",
"version": "0.1.0",
"description": "🎧 a self-hosted Discord music bot that doesn't suck ",
"main": "dist/index.js",
"repository": "git@github.com:codetheweb/muse.git",
"author": "Max Isom <hi@maxisom.me>",
"license": "MIT",
"private": true,
"types": "dts/types",
"files": [
"dist",
"dts"
],
"scripts": {
"lint": "eslint 'src/**/*.ts'",
"lint-fix": "eslint 'src/**/*.ts' --fix",
"clean": "rm -rf dist dts",
"test": "npm run lint",
"build": "tsc",
"watch": "tsc --watch",
"prepack": "npm run clean && npm run build",
"start": "node dist/index.js",
"dev": "nodemon"
},
"devDependencies": {
"@types/bluebird": "^3.5.30",
"@types/node": "^13.9.0",
"@types/validator": "^12.0.1",
"@types/ws": "^7.2.2",
"@typescript-eslint/eslint-plugin": "^2.22.0",
"@typescript-eslint/parser": "^2.22.0",
"eslint": "^6.8.0",
"eslint-config-xo": "^0.29.1",
"eslint-config-xo-typescript": "^0.26.0",
"husky": "^4.2.3",
"nodemon": "^2.0.2",
"reflect-metadata": "^0.1.13",
"ts-node": "^8.6.2",
"typescript": "^3.8.3"
},
"eslintConfig": {
"extends": [
"xo",
"xo-typescript/space"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"new-cap": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-vars-experimental": "error",
"@typescript-eslint/no-inferrable-types": "off"
}
},
"husky": {
"hooks": {
"pre-commit": "npm test && npm run build"
}
},
"dependencies": {
"@discordjs/opus": "^0.1.0",
"discord.js": "^12.0.1",
"dotenv": "^8.2.0",
"make-dir": "^3.0.2",
"node-emoji": "^1.10.0",
"sequelize": "^5.21.5",
"sequelize-typescript": "^1.1.0",
"sqlite3": "^4.1.1"
}
}

24
src/commands/config.ts Normal file
View file

@ -0,0 +1,24 @@
import {CommandHandler} from '../interfaces';
const config: CommandHandler = {
name: 'config',
description: 'Change various bot settings.',
execute: (msg, args) => {
const setting = args[0];
switch (setting) {
case 'prefix':
msg.channel.send('Prefix set');
break;
case 'channel':
msg.channel.send('Channel bound');
break;
default:
msg.channel.send('Unknown setting');
}
}
};
export default config;

View file

@ -0,0 +1,73 @@
import {Guild, MessageReaction, TextChannel} from 'discord.js';
import emoji from 'node-emoji';
import {Settings} from '../models';
const DEFAULT_PREFIX = '!';
export default async (guild: Guild): Promise<void> => {
await Settings.upsert({guildId: guild.id, prefix: DEFAULT_PREFIX});
const owner = await guild.client.users.fetch(guild.ownerID);
let firstStep = '👋 Hi!\n';
firstStep += 'I just need to ask a few questions before you start listening to music.\n\n';
firstStep += 'First, what channel should I listen to for music commands?\n\n';
interface EmojiChannel {
name: string;
id: string;
emoji: string;
}
const emojiChannels: EmojiChannel[] = [];
for (const [channelId, channel] of guild.channels.cache) {
if (channel.type === 'text') {
emojiChannels.push({
name: channel.name,
id: channelId,
emoji: emoji.random().emoji
});
}
}
for (const channel of emojiChannels) {
firstStep += `${channel.emoji}: #${channel.name}\n`;
}
firstStep += '\n';
// Send message
const msg = await owner.send(firstStep);
// Add reactions
for await (const channel of emojiChannels) {
await msg.react(channel.emoji);
}
const reactions = await msg.awaitReactions((reaction, user) => user.id !== msg.author.id && emojiChannels.map(e => e.emoji).includes(reaction.emoji.name), {max: 1});
const choice = reactions.first() as MessageReaction;
const chosenChannel = emojiChannels.find(e => e.emoji === choice.emoji.name) as EmojiChannel;
// Second setup step (get prefix)
let secondStep = `👍 Cool, I'll listen to **#${chosenChannel.name}** \n\n`;
secondStep += 'Last question: what character should I use for a prefix? Type a single character and hit enter.';
await owner.send(secondStep);
const prefixResponses = await msg.channel.awaitMessages(r => r.content.length === 1, {max: 1});
const prefixCharacter = prefixResponses.first()!.content;
// Save settings
await Settings.update({prefix: prefixCharacter, channel: chosenChannel.id}, {where: {guildId: guild.id}});
// Send welcome
const boundChannel = guild.client.channels.cache.get(chosenChannel.id) as TextChannel;
await boundChannel.send(`hey <@${owner.id}> try \`${prefixCharacter}play https://www.youtube.com/watch?v=dQw4w9WgXcQ\``);
await msg.channel.send(`Sounds good. Check out **#${chosenChannel.name}** to get started.`);
};

58
src/index.ts Normal file
View file

@ -0,0 +1,58 @@
import fs from 'fs';
import path from 'path';
import makeDir from 'make-dir';
import Discord from 'discord.js';
import {DISCORD_TOKEN, DISCORD_CLIENT_ID, DATA_DIR} from './utils/config';
import {sequelize} from './utils/db';
import {CommandHandler} from './interfaces';
import handleGuildCreate from './events/guild-create';
const PREFIX = '!';
const client = new Discord.Client();
const commands = new Discord.Collection();
// Load in commands
const commandFiles = fs.readdirSync(path.join(__dirname, 'commands')).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`).default;
commands.set(command.name, command);
}
// Generic message handler
client.on('message', (msg: Discord.Message) => {
if (!msg.content.startsWith(PREFIX) || msg.author.bot) {
return;
}
const args = msg.content.slice(PREFIX.length).split(/ +/);
const command = args.shift()!.toLowerCase();
if (!commands.has(command)) {
return;
}
try {
const handler = commands.get(command) as CommandHandler;
handler.execute(msg, args);
} catch (error) {
console.error(error);
msg.reply('there was an error trying to execute that command!');
}
});
client.on('ready', async () => {
// Create directory if necessary
await makeDir(DATA_DIR);
await sequelize.sync({});
console.log(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&scope=bot`);
});
client.on('guildCreate', handleGuildCreate);
client.login(DISCORD_TOKEN);

7
src/interfaces.ts Normal file
View file

@ -0,0 +1,7 @@
import Discord from 'discord.js';
export interface CommandHandler {
name: string;
description: string;
execute: (msg: Discord.Message, args: string[]) => void;
}

5
src/models/index.ts Normal file
View file

@ -0,0 +1,5 @@
import Settings from './settings';
export {
Settings
};

18
src/models/settings.ts Normal file
View file

@ -0,0 +1,18 @@
import {Table, Column, PrimaryKey, Model, Default} from 'sequelize-typescript';
@Table
export default class Settings extends Model<Settings> {
@PrimaryKey
@Column
guildId!: string;
@Column
prefix!: string;
@Column
channel!: string;
@Default(false)
@Column
finishedSetup!: boolean;
}

1
src/packages.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module 'node-emoji';

7
src/utils/config.ts Normal file
View file

@ -0,0 +1,7 @@
import dotenv from 'dotenv';
import path from 'path';
dotenv.config();
export const DISCORD_TOKEN: string = process.env.DISCORD_TOKEN ? process.env.DISCORD_TOKEN : '';
export const DISCORD_CLIENT_ID: string = process.env.DISCORD_CLIENT_ID ? process.env.DISCORD_CLIENT_ID : '';
export const DATA_DIR = path.resolve(process.env.DATA_DIR ? process.env.DATA_DIR : './data');

11
src/utils/db.ts Normal file
View file

@ -0,0 +1,11 @@
import {Sequelize} from 'sequelize-typescript';
import path from 'path';
import {DATA_DIR} from '../utils/config';
import {Settings} from '../models';
export const sequelize = new Sequelize({
dialect: 'sqlite',
database: 'muse',
storage: path.join(DATA_DIR, 'db.sqlite'),
models: [Settings]
});

17
tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"lib": ["ES2017"],
"target": "es6",
"module": "commonjs",
"declaration": true,
"outDir": "dist",
"declarationDir": "dts",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

2579
yarn.lock Normal file

File diff suppressed because it is too large Load diff