Add better responses

This commit is contained in:
Max Isom 2020-03-17 17:59:26 -05:00
parent 1a1bdfd674
commit 15d4e251f2
17 changed files with 103 additions and 63 deletions

View file

@ -21,10 +21,12 @@
"watch": "tsc --watch", "watch": "tsc --watch",
"prepack": "npm run clean && npm run build", "prepack": "npm run clean && npm run build",
"start": "node dist/index.js", "start": "node dist/index.js",
"dev": "nodemon" "dev": "nodemon",
"docker-publish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t codetheweb/muse:latest --push ."
}, },
"devDependencies": { "devDependencies": {
"@types/bluebird": "^3.5.30", "@types/bluebird": "^3.5.30",
"@types/debug": "^4.1.5",
"@types/fluent-ffmpeg": "^2.1.14", "@types/fluent-ffmpeg": "^2.1.14",
"@types/fs-capacitor": "^2.0.0", "@types/fs-capacitor": "^2.0.0",
"@types/node": "^13.9.1", "@types/node": "^13.9.1",
@ -65,6 +67,7 @@
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.1.0", "@discordjs/opus": "^0.1.0",
"array-shuffle": "^1.0.1", "array-shuffle": "^1.0.1",
"debug": "^4.1.1",
"delay": "^4.3.0", "delay": "^4.3.0",
"discord.js": "^12.0.2", "discord.js": "^12.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",

View file

@ -4,6 +4,7 @@ import {TYPES} from './types';
import {Settings, Shortcut} from './models'; import {Settings, Shortcut} from './models';
import container from './inversify.config'; import container from './inversify.config';
import Command from './commands'; import Command from './commands';
import debug from './utils/debug';
import handleGuildCreate from './events/guild-create'; import handleGuildCreate from './events/guild-create';
import handleVoiceStateUpdate from './events/voice-state-update'; import handleVoiceStateUpdate from './events/voice-state-update';
@ -89,6 +90,7 @@ export default class {
}); });
this.client.on('error', console.error); this.client.on('error', console.error);
this.client.on('debug', debug);
// Register event handlers // Register event handlers
this.client.on('guildCreate', handleGuildCreate); this.client.on('guildCreate', handleGuildCreate);

View file

@ -20,6 +20,6 @@ export default class implements Command {
public async execute(msg: Message, _: string []): Promise<void> { public async execute(msg: Message, _: string []): Promise<void> {
this.queueManager.get(msg.guild!.id).clear(); this.queueManager.get(msg.guild!.id).clear();
await msg.channel.send('cleared'); await msg.channel.send('clearer than a field after a fresh harvest');
} }
} }

View file

@ -1,6 +1,7 @@
import {TextChannel, Message} from 'discord.js'; import {TextChannel, Message} from 'discord.js';
import {injectable} from 'inversify'; import {injectable} from 'inversify';
import {Settings} from '../models'; import {Settings} from '../models';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
@ -29,12 +30,12 @@ export default class implements Command {
const setting = args[0]; const setting = args[0];
if (args.length !== 2) { if (args.length !== 2) {
await msg.channel.send('🚫 incorrect number of arguments'); await msg.channel.send(errorMsg('incorrect number of arguments'));
return; return;
} }
if (msg.author.id !== msg.guild!.owner!.id) { if (msg.author.id !== msg.guild!.owner!.id) {
await msg.channel.send('not authorized'); await msg.channel.send(errorMsg('not authorized'));
return; return;
} }
@ -59,14 +60,14 @@ export default class implements Command {
msg.react('👍') msg.react('👍')
]); ]);
} else { } else {
await msg.channel.send('🚫 either that channel doesn\'t exist or you want me to become sentient and listen to a voice channel'); await msg.channel.send(errorMsg('either that channel doesn\'t exist or you want me to become sentient and listen to a voice channel'));
} }
break; break;
} }
default: default:
await msg.channel.send('🚫 I\'ve never met this setting in my life'); await msg.channel.send(errorMsg('I\'ve never met this setting in my life'));
} }
} }
} }

View file

@ -4,6 +4,7 @@ import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player'; import PlayerManager from '../managers/player';
import QueueManager from '../managers/queue'; import QueueManager from '../managers/queue';
import LoadingMessage from '../utils/loading-message'; import LoadingMessage from '../utils/loading-message';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
@ -25,27 +26,27 @@ export default class implements Command {
const queue = this.queueManager.get(msg.guild!.id); const queue = this.queueManager.get(msg.guild!.id);
if (queue.get().length === 0) { if (queue.get().length === 0) {
await msg.channel.send('nothing is playing'); await msg.channel.send(errorMsg('nothing is playing'));
return; return;
} }
if (queue.get()[0].isLive) { if (queue.get()[0].isLive) {
await msg.channel.send('can\'t seek in a livestream'); await msg.channel.send(errorMsg('can\'t seek in a livestream'));
return; return;
} }
const seekTime = parseInt(args[0], 10); const seekTime = parseInt(args[0], 10);
const loading = new LoadingMessage(msg.channel as TextChannel, 'hold on a sec'); const loading = new LoadingMessage(msg.channel as TextChannel);
await loading.start(); await loading.start();
try { try {
await this.playerManager.get(msg.guild!.id).forwardSeek(seekTime); await this.playerManager.get(msg.guild!.id).forwardSeek(seekTime);
await loading.stop('seeked'); await loading.stop();
} catch (_) { } catch (error) {
await loading.stop('error somewhere'); await loading.stop(errorMsg(error));
} }
} }
} }

View file

@ -3,6 +3,7 @@ import {TYPES} from '../types';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player'; import PlayerManager from '../managers/player';
import {STATUS} from '../services/player'; import {STATUS} from '../services/player';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
@ -22,11 +23,11 @@ export default class implements Command {
const player = this.playerManager.get(msg.guild!.id); const player = this.playerManager.get(msg.guild!.id);
if (player.status !== STATUS.PLAYING) { if (player.status !== STATUS.PLAYING) {
await msg.channel.send('error: not currently playing'); await msg.channel.send(errorMsg('not currently playing'));
return; return;
} }
player.pause(); player.pause();
await msg.channel.send('paused'); await msg.channel.send('the stop-and-go light is now red');
} }
} }

View file

@ -15,6 +15,7 @@ import QueueManager from '../managers/queue';
import PlayerManager from '../managers/player'; import PlayerManager from '../managers/player';
import {getMostPopularVoiceChannel} from '../utils/channels'; import {getMostPopularVoiceChannel} from '../utils/channels';
import LoadingMessage from '../utils/loading-message'; import LoadingMessage from '../utils/loading-message';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
@ -47,8 +48,11 @@ export default class implements Command {
public async execute(msg: Message, args: string []): Promise<void> { public async execute(msg: Message, args: string []): Promise<void> {
const [targetVoiceChannel, nInChannel] = getMostPopularVoiceChannel(msg.guild!); const [targetVoiceChannel, nInChannel] = getMostPopularVoiceChannel(msg.guild!);
const res = new LoadingMessage(msg.channel as TextChannel);
await res.start();
if (nInChannel === 0) { if (nInChannel === 0) {
await msg.channel.send('error: all voice channels are empty'); await res.stop(errorMsg('all voice channels are empty'));
return; return;
} }
@ -56,28 +60,25 @@ export default class implements Command {
if (args.length === 0) { if (args.length === 0) {
if (this.playerManager.get(msg.guild!.id).status === STATUS.PLAYING) { if (this.playerManager.get(msg.guild!.id).status === STATUS.PLAYING) {
await msg.channel.send('error: already playing, give me a song name'); await res.stop(errorMsg('already playing, give me a song name'));
return; return;
} }
// Must be resuming play // Must be resuming play
if (queue.get().length === 0) { if (queue.get().length === 0) {
await msg.channel.send('error: nothing to play'); await res.stop(errorMsg('nothing to play'));
return; return;
} }
await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel); await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel);
await this.playerManager.get(msg.guild!.id).play(); await this.playerManager.get(msg.guild!.id).play();
await msg.channel.send('play resuming'); await res.stop('play resuming');
return; return;
} }
const newSongs: QueuedSong[] = []; const newSongs: QueuedSong[] = [];
const res = new LoadingMessage(msg.channel as TextChannel, 'hold on a sec');
await res.start();
const addSingleSong = async (source: string): Promise<void> => { const addSingleSong = async (source: string): Promise<void> => {
const videoDetails = await this.youtube.videos.get(source); const videoDetails = await this.youtube.videos.get(source);
@ -265,7 +266,7 @@ export default class implements Command {
// TODO: better response // TODO: better response
await res.stop('song(s) queued'); await res.stop('song(s) queued');
if (this.playerManager.get(msg.guild!.id).status === STATUS.DISCONNECTED) { if (this.playerManager.get(msg.guild!.id).voiceConnection === null) {
await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel); await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel);
await this.playerManager.get(msg.guild!.id).play(); await this.playerManager.get(msg.guild!.id).play();

View file

@ -4,6 +4,7 @@ import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player'; import PlayerManager from '../managers/player';
import QueueManager from '../managers/queue'; import QueueManager from '../managers/queue';
import LoadingMessage from '../utils/loading-message'; import LoadingMessage from '../utils/loading-message';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
@ -26,12 +27,12 @@ export default class implements Command {
const queue = this.queueManager.get(msg.guild!.id); const queue = this.queueManager.get(msg.guild!.id);
if (queue.get().length === 0) { if (queue.get().length === 0) {
await msg.channel.send('nothing is playing'); await msg.channel.send(errorMsg('nothing is playing'));
return; return;
} }
if (queue.get()[0].isLive) { if (queue.get()[0].isLive) {
await msg.channel.send('can\'t seek in a livestream'); await msg.channel.send(errorMsg('can\'t seek in a livestream'));
return; return;
} }
@ -45,16 +46,16 @@ export default class implements Command {
seekTime = parseInt(time, 10); seekTime = parseInt(time, 10);
} }
const loading = new LoadingMessage(msg.channel as TextChannel, 'hold on a sec'); const loading = new LoadingMessage(msg.channel as TextChannel);
await loading.start(); await loading.start();
try { try {
await this.playerManager.get(msg.guild!.id).seek(seekTime); await this.playerManager.get(msg.guild!.id).seek(seekTime);
await loading.stop('seeked'); await loading.stop();
} catch (_) { } catch (error) {
await loading.stop('error somewhere'); await loading.stop(errorMsg(error));
} }
} }
} }

View file

@ -1,12 +1,14 @@
import {Message} from 'discord.js'; import {Message} from 'discord.js';
import {injectable} from 'inversify'; import {injectable} from 'inversify';
import {Shortcut, Settings} from '../models'; import {Shortcut, Settings} from '../models';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
export default class implements Command { export default class implements Command {
public name = 'shortcuts'; public name = 'shortcuts';
public examples = [ public examples = [
['shortcuts', 'show all shortcuts'],
['shortcuts set s skip', 'aliases `s` to `skip`'], ['shortcuts set s skip', 'aliases `s` to `skip`'],
['shortcuts set party play https://www.youtube.com/watch?v=zK6oOJ1wz8k', 'aliases `party` to a specific play command'], ['shortcuts set party play https://www.youtube.com/watch?v=zK6oOJ1wz8k', 'aliases `party` to a specific play command'],
['shortcuts delete party', 'removes the `party` shortcut'] ['shortcuts delete party', 'removes the `party` shortcut']
@ -53,7 +55,7 @@ export default class implements Command {
if (shortcut) { if (shortcut) {
if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.owner!.id) { if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.owner!.id) {
await msg.channel.send('error: you do not have permission to do that'); await msg.channel.send(errorMsg('you do\'nt have permission to do that'));
return; return;
} }
@ -72,13 +74,13 @@ export default class implements Command {
const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}}); const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}});
if (!shortcut) { if (!shortcut) {
await msg.channel.send('error: shortcut does not exist'); await msg.channel.send(errorMsg('shortcut doesn\'t exist'));
return; return;
} }
// Check permissions // Check permissions
if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.owner!.id) { if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.owner!.id) {
await msg.channel.send('error: you do not have permission to do that'); await msg.channel.send(errorMsg('you don\'t have permission to do that'));
return; return;
} }
@ -90,7 +92,7 @@ export default class implements Command {
} }
default: { default: {
await msg.channel.send('error: unknown command'); await msg.channel.send(errorMsg('unknown command'));
} }
} }
} }

View file

@ -2,6 +2,7 @@ import {Message} from 'discord.js';
import {TYPES} from '../types'; import {TYPES} from '../types';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import QueueManager from '../managers/queue'; import QueueManager from '../managers/queue';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
@ -21,12 +22,13 @@ export default class implements Command {
const queue = this.queueManager.get(msg.guild!.id).get(); const queue = this.queueManager.get(msg.guild!.id).get();
if (queue.length <= 2) { if (queue.length <= 2) {
await msg.channel.send('error: not enough songs to shuffle'); await msg.channel.send(errorMsg('not enough songs to shuffle'));
return; return;
} }
this.queueManager.get(msg.guild!.id).shuffle(); this.queueManager.get(msg.guild!.id).shuffle();
await msg.channel.send('`' + JSON.stringify(this.queueManager.get(msg.guild!.id).get().slice(0, 10)) + '`'); // TODO: better response
await msg.channel.send('shuffled');
} }
} }

View file

@ -32,9 +32,8 @@ export default class implements Command {
await this.playerManager.get(msg.guild!.id).play(); await this.playerManager.get(msg.guild!.id).play();
} }
await msg.channel.send('keepin\' \'er movin\''); await msg.channel.send('keep \'er movin\'');
} catch (_) { } catch (_) {
console.log(_);
await msg.channel.send('no song to skip to'); await msg.channel.send('no song to skip to');
} }
} }

View file

@ -3,6 +3,7 @@ import {TYPES} from '../types';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player'; import PlayerManager from '../managers/player';
import QueueManager from '../managers/queue'; import QueueManager from '../managers/queue';
import errorMsg from '../utils/error-msg';
import Command from '.'; import Command from '.';
@injectable() @injectable()
@ -30,7 +31,7 @@ export default class implements Command {
await msg.channel.send('back \'er up\''); await msg.channel.send('back \'er up\'');
} catch (_) { } catch (_) {
await msg.channel.send('no song to go back to'); await msg.channel.send(errorMsg('no song to go back to'));
} }
} }
} }

View file

@ -10,12 +10,11 @@ import Queue, {QueuedSong} from './queue';
export enum STATUS { export enum STATUS {
PLAYING, PLAYING,
PAUSED, PAUSED
DISCONNECTED
} }
export default class { export default class {
public status = STATUS.DISCONNECTED; public status = STATUS.PAUSED;
public voiceConnection: VoiceConnection | null = null; public voiceConnection: VoiceConnection | null = null;
private readonly queue: Queue; private readonly queue: Queue;
private readonly cacheDir: string; private readonly cacheDir: string;
@ -35,17 +34,24 @@ export default class {
this.voiceConnection = conn; this.voiceConnection = conn;
} }
disconnect(): void { disconnect(breakConnection = true): void {
if (this.voiceConnection) { if (this.voiceConnection) {
if (this.status === STATUS.PLAYING) { if (this.status === STATUS.PLAYING) {
this.pause(); this.pause();
} }
if (breakConnection) {
this.voiceConnection.disconnect(); this.voiceConnection.disconnect();
} }
this.voiceConnection = null;
this.dispatcher = null;
}
} }
async seek(positionSeconds: number): Promise<void> { async seek(positionSeconds: number): Promise<void> {
this.status = STATUS.PAUSED;
if (this.voiceConnection === null) { if (this.voiceConnection === null) {
throw new Error('Not connected to a voice channel.'); throw new Error('Not connected to a voice channel.');
} }
@ -79,24 +85,28 @@ export default class {
throw new Error('Not connected to a voice channel.'); throw new Error('Not connected to a voice channel.');
} }
// Resume from paused state
if (this.status === STATUS.PAUSED) {
if (this.dispatcher) {
this.dispatcher.resume();
this.status = STATUS.PLAYING;
} else {
await this.seek(this.getPosition());
}
return;
}
const currentSong = this.getCurrentSong(); const currentSong = this.getCurrentSong();
if (!currentSong) { if (!currentSong) {
throw new Error('Queue empty.'); throw new Error('Queue empty.');
} }
// Resume from paused state
if (this.status === STATUS.PAUSED && this.getPosition() !== 0) {
if (this.dispatcher) {
this.dispatcher.resume();
this.status = STATUS.PLAYING;
return;
}
if (!currentSong.isLive) {
await this.seek(this.getPosition());
return;
}
// Must be livestream, continue
}
if (await this.isCached(currentSong.url)) { if (await this.isCached(currentSong.url)) {
this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url)); this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url));
} else { } else {
@ -153,7 +163,7 @@ export default class {
} }
} }
private async waitForCache(url: string, maxRetries = 50, retryDelay = 500): Promise<void> { private async waitForCache(url: string, maxRetries = 500, retryDelay = 200): Promise<void> {
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (await this.isCached(url)) { if (await this.isCached(url)) {
@ -278,12 +288,7 @@ export default class {
} }
this.voiceConnection.on('disconnect', () => { this.voiceConnection.on('disconnect', () => {
// Automatically pause this.disconnect(false);
if (this.status === STATUS.PLAYING) {
this.pause();
}
this.dispatcher = null;
}); });
if (!this.dispatcher) { if (!this.dispatcher) {

3
src/utils/debug.ts Normal file
View file

@ -0,0 +1,3 @@
import debug from 'debug';
export default debug('muse');

13
src/utils/error-msg.ts Normal file
View file

@ -0,0 +1,13 @@
export default (error?: string | Error): string => {
let str = '🚫 unknown error';
if (error) {
if (typeof error === 'string') {
str = `🚫 ${error}`;
} else if (error instanceof Error) {
str = `🚫 error: ${error.name}`;
}
}
return str;
};

View file

@ -7,7 +7,7 @@ export default class {
private msg!: Message; private msg!: Message;
private isStopped = false; private isStopped = false;
constructor(channel: TextChannel, text: string) { constructor(channel: TextChannel, text = 'cows! count \'em') {
this.channel = channel; this.channel = channel;
this.text = text; this.text = text;
} }
@ -53,7 +53,7 @@ export default class {
})(); })();
} }
async stop(str?: string): Promise<Message> { async stop(str = 'u betcha'): Promise<Message> {
this.isStopped = true; this.isStopped = true;
if (str) { if (str) {

View file

@ -70,6 +70,11 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/debug@^4.1.5":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
"@types/eslint-visitor-keys@^1.0.0": "@types/eslint-visitor-keys@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"