1590 lines
61 KiB
JavaScript
1590 lines
61 KiB
JavaScript
|
/**
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*
|
||
|
* MIT License
|
||
|
*
|
||
|
* Copyright (c) 2019-2020 Jonas Bögle, Michael Friese
|
||
|
*
|
||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
* of this software and associated documentation files (the "Software"), to deal
|
||
|
* in the Software without restriction, including without limitation the rights
|
||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
* copies of the Software, and to permit persons to whom the Software is
|
||
|
* furnished to do so, subject to the following conditions:
|
||
|
*
|
||
|
* The above copyright notice and this permission notice shall be included in all
|
||
|
* copies or substantial portions of the Software.
|
||
|
*
|
||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
* SOFTWARE.
|
||
|
*
|
||
|
* Thanks to the following GitHub sponsors for supporting my work:
|
||
|
* - Michael Friese
|
||
|
* - Jay Vasallo
|
||
|
*
|
||
|
* https://github.com/sponsors/irgendwr
|
||
|
*
|
||
|
*/
|
||
|
registerPlugin({
|
||
|
name: 'SinusBot Commands',
|
||
|
version: '1.1.2',
|
||
|
description: 'Enables the default commands.',
|
||
|
author: 'Jonas Bögle (@irgendwr)',
|
||
|
engine: '>= 1.0.0',
|
||
|
backends: ['ts3', 'discord'],
|
||
|
// the next line is not required since beta.7
|
||
|
//requiredModules: ['discord-dangerous'],
|
||
|
autorun: true,
|
||
|
vars: [
|
||
|
{
|
||
|
/*
|
||
|
* Note: Normally you should **not** add something like this,
|
||
|
* because you can already disable scripts by unchecking them.
|
||
|
* However in this case it makes sense as `autorun: true`
|
||
|
* no longer allows users to disable it otherwise.
|
||
|
*/
|
||
|
name: 'disable',
|
||
|
title: 'Disable the default SinusBot commands.',
|
||
|
type: 'checkbox',
|
||
|
default: false
|
||
|
},
|
||
|
{
|
||
|
name: 'createSuccessReaction',
|
||
|
title: 'Add a reaction to each command if it was successfull.',
|
||
|
type: 'checkbox',
|
||
|
default: false,
|
||
|
/* conditions: [
|
||
|
{ field: 'disable', value: false }
|
||
|
], */ // conditions checking for "false" are currently buggy. @flyth needs to fix this.
|
||
|
},
|
||
|
{
|
||
|
name: 'discord',
|
||
|
title: 'Show discord settings',
|
||
|
type: 'checkbox',
|
||
|
default: true,
|
||
|
/* conditions: [
|
||
|
{ field: 'disable', value: false }
|
||
|
], */ // conditions checking for "false" are currently buggy. @flyth needs to fix this.
|
||
|
},
|
||
|
{
|
||
|
name: 'url',
|
||
|
title: 'URL to Webinterface (optional, for album covers in discord)',
|
||
|
type: 'string',
|
||
|
placeholder: 'i.e. https://sinusbot.example.com',
|
||
|
conditions: [
|
||
|
{ field: 'discord', value: true },
|
||
|
/*{ field: 'disable', value: false }*/
|
||
|
], // conditions checking for "false" are currently buggy. @flyth needs to fix this.
|
||
|
},
|
||
|
{
|
||
|
name: 'songInStatus',
|
||
|
title: 'Show playing song in status.',
|
||
|
type: 'checkbox',
|
||
|
default: true,
|
||
|
conditions: [
|
||
|
{ field: 'discord', value: true }
|
||
|
],
|
||
|
},
|
||
|
{
|
||
|
name: 'deleteOldMessages',
|
||
|
title: 'Delete previous responses if !playing command is used again',
|
||
|
type: 'checkbox',
|
||
|
default: true,
|
||
|
conditions: [
|
||
|
{ field: 'discord', value: true },
|
||
|
/*{ field: 'disable', value: false }*/
|
||
|
], // conditions checking for "false" are currently buggy. @flyth needs to fix this.
|
||
|
}
|
||
|
]
|
||
|
}, (_, config, meta) => {
|
||
|
const event = require('event')
|
||
|
const engine = require('engine')
|
||
|
const backend = require('backend')
|
||
|
const format = require('format')
|
||
|
const audio = require('audio')
|
||
|
const media = require('media')
|
||
|
|
||
|
engine.log(`Loaded ${meta.name} v${meta.version} by ${meta.author}.`)
|
||
|
engine.log(`SinusBot v${engine.version()} on ${engine.os()}`)
|
||
|
|
||
|
/********* privileges *********/
|
||
|
const ENQUEUE = 1 << 13;
|
||
|
const SKIP_QUEUE = 1 << 14;
|
||
|
const ADMIN_QUEUE = 1 << 15;
|
||
|
const PLAYBACK = 1 << 12;
|
||
|
const START_STOP = 1 << 8;
|
||
|
const EDIT_BOT_SETTINGS = 1 << 16;
|
||
|
const LOGIN = 1 << 0;
|
||
|
const UPLOAD_FILES = 1 << 2;
|
||
|
const DELETE_FILES = 1 << 3;
|
||
|
const EDIT_FILES = 1 << 4;
|
||
|
const CREATE_AND_DELETE_PLAYLISTS = 1 << 5;
|
||
|
const EDIT_PLAYLISTS = 1 << 7;
|
||
|
const EDIT_INSTANCES = 1 << 17;
|
||
|
const EDIT_USERS = 1 << 9;
|
||
|
|
||
|
const ERROR_PREFIX = '❌ ';
|
||
|
const WARNING_PREFIX = '⚠ ';
|
||
|
const SUCCESS_PREFIX = '✔ ';
|
||
|
const USAGE_PREFIX = ERROR_PREFIX + 'Usage: ';
|
||
|
|
||
|
const sinusbotURL = config.url;
|
||
|
const REACTION_PREV = '⏮';
|
||
|
const REACTION_PLAYPAUSE = '⏯';
|
||
|
const REACTION_NEXT = '⏭';
|
||
|
const REACTION_SUCCESS = '✅';
|
||
|
const HIDE_REACTIONS_IF_PRIVATE = false;
|
||
|
|
||
|
const PATTERN_URL = /^https?:\/\/\S+/i;
|
||
|
const PATTERN_YT_DOMAIN = /^https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\//i;
|
||
|
|
||
|
// for join/leave
|
||
|
const ERROR_BOT_NULL = ERROR_PREFIX+'Unable to change channel.\nTry to set a *Default Channel* in the webinterface and click save.'
|
||
|
|
||
|
/** @type {object[]} */
|
||
|
let lastEmbeds = [];
|
||
|
|
||
|
if (config.discord && engine.getBackend() != 'discord') {
|
||
|
// hide discord-only settings if backend is not discord
|
||
|
config.discord = false;
|
||
|
engine.saveConfig(config);
|
||
|
} else if (!config.discord && engine.getBackend() == 'discord') {
|
||
|
// show discord-only settings if backend is discord
|
||
|
config.discord = true;
|
||
|
engine.saveConfig(config);
|
||
|
}
|
||
|
|
||
|
const ytCallbacks = {};
|
||
|
event.on("ytdl.success", ev => {
|
||
|
engine.log(`Downloaded YouTube Video: ${ev.url}`);
|
||
|
const jobId = ev.jobId;
|
||
|
if (typeof ytCallbacks[jobId] === 'function') {
|
||
|
ytCallbacks[jobId](ev);
|
||
|
delete ytCallbacks[jobId];
|
||
|
}
|
||
|
});
|
||
|
|
||
|
event.on("ytdl.error", (ev, message) => {
|
||
|
engine.log(`Error while downloading YouTube Video: ${ev.url}; ${message}`);
|
||
|
if (message.startsWith("exit status")) {
|
||
|
engine.log('Please see "Upload" page in web-interface for more details and read the documentation for troubleshooting advice: https://sinusbot.github.io/docs/youtube-dl/');
|
||
|
}
|
||
|
|
||
|
const jobId = ev.jobId;
|
||
|
if (typeof ytCallbacks[jobId] === 'function') {
|
||
|
ytCallbacks[jobId](ev, message);
|
||
|
delete ytCallbacks[jobId];
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Registers a callback for success/error events of a given jobId.
|
||
|
* @param {string} jobId Job ID
|
||
|
* @param {(ev: {url: string, jobId: string, trackId?: string}, message: string) => void} callback Callback
|
||
|
*/
|
||
|
function ytCallback(jobId, callback) {
|
||
|
ytCallbacks[jobId] = callback;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param {string} jobId Job ID
|
||
|
* @param {MessageEvent} ev Event
|
||
|
* @param {(msg: string) => void} reply
|
||
|
*/
|
||
|
function handleYT(jobId, ev, reply) {
|
||
|
ytCallback(jobId, (ytev, err) => {
|
||
|
if (err) {
|
||
|
if (err.startsWith("exit status")) {
|
||
|
reply(ERROR_PREFIX + `Error: ${err}; Please see "Upload" page in web-interface for more details.`);
|
||
|
} else {
|
||
|
reply(ERROR_PREFIX + `Error: ${err}`);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (config.disable) {
|
||
|
engine.log('SinusBot commands are DISABLED.');
|
||
|
} else { // BEGIN COMMANDS-ENABLED
|
||
|
event.on('load', () => {
|
||
|
const command = require('command');
|
||
|
if (!command) {
|
||
|
engine.log('command.js library not found! Please download command.js to your scripts folder and restart the SinusBot, otherwise this script will not work.');
|
||
|
engine.log('command.js can be found here: https://github.com/Multivit4min/Sinusbot-Command/blob/master/command.js');
|
||
|
return;
|
||
|
}
|
||
|
const {createCommand} = command;
|
||
|
|
||
|
createCommand('register')
|
||
|
.addArgument(args => args.string.setName('username'))
|
||
|
.help('Register a new user')
|
||
|
.manual('Registers a new user bound to the Account you are using. This account has no privileges by default but can be edited by the bot administrators.')
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (!engine.registrationEnabled()) {
|
||
|
reply('Registration is disabled.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// print syntax if no username given
|
||
|
if (!args.username) {
|
||
|
reply(USAGE_PREFIX + 'register <username>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (engine.getUserByName(args.username)) {
|
||
|
reply(ERROR_PREFIX + 'This username already exists.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// check if client already has a user
|
||
|
let user = getUserByUid(client.uid());
|
||
|
if (user) {
|
||
|
reply(ERROR_PREFIX + `You already have a user with the name "${user.name()}".`);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// create user
|
||
|
let newUser = engine.addUser(args.username);
|
||
|
if (!newUser) {
|
||
|
reply(ERROR_PREFIX + 'Unable to create user, try another username.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!newUser.setUid(client.uid())) {
|
||
|
newUser.delete()
|
||
|
reply(ERROR_PREFIX + 'Unable to assign uid to user.');
|
||
|
return;
|
||
|
}
|
||
|
reply(SUCCESS_PREFIX + 'Registered a user with the given name.\nThis account has no privileges by default but can be edited by the bot administrators.');
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('password')
|
||
|
.alias('pass')
|
||
|
.addArgument(args => args.rest.setName('value'))
|
||
|
.help('Change your password')
|
||
|
.manual('Changes your password to <value>.')
|
||
|
.checkPermission(client => getUserByUid(client.uid()))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no value given
|
||
|
if (!args.value) {
|
||
|
reply(USAGE_PREFIX + 'password <value>\n'+ WARNING_PREFIX + 'Don\'t use this command in a public channel.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (ev.mode !== 1) {
|
||
|
reply(WARNING_PREFIX + 'Don\'t use this command in a public channel.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let user = getUserByUid(client.uid());
|
||
|
if (!user) {
|
||
|
reply(ERROR_PREFIX + `You don't have a user-account. Use ${format.bold('!register')} to create one.`);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// set password
|
||
|
if (!user.setPassword(args.value)) {
|
||
|
reply(ERROR_PREFIX + 'Unable to set password.');
|
||
|
return;
|
||
|
}
|
||
|
reply(SUCCESS_PREFIX + 'Changed your password.');
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('whoami')
|
||
|
.help('Show user identities')
|
||
|
.manual('Shows user identities matching your ID/groups.')
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
let users = getUsersByClient(client);
|
||
|
if (users && users.length != 0) {
|
||
|
const usersStr = users.map(user => user.name()).join(", ");
|
||
|
if (users.length == 1) {
|
||
|
reply(`You match the following user: ${usersStr}.`);
|
||
|
} else {
|
||
|
reply(`You match the following ${users.length} users: ${usersStr}.`);
|
||
|
}
|
||
|
} else {
|
||
|
reply("You don't match any users.");
|
||
|
}
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('privileges')
|
||
|
.alias('priv', 'privs')
|
||
|
.help('Show user privileges')
|
||
|
.manual('Shows user privileges.')
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
let privs = [
|
||
|
{ n: ENQUEUE, s: 'Enqueue' },
|
||
|
{ n: SKIP_QUEUE, s: 'Skip Queue' },
|
||
|
{ n: ADMIN_QUEUE, s: 'Admin Queue' },
|
||
|
{ n: PLAYBACK, s: 'Playback' },
|
||
|
{ n: START_STOP, s: 'Start/Stop' },
|
||
|
{ n: EDIT_BOT_SETTINGS, s: 'Edit Bot Settings' },
|
||
|
{ n: LOGIN, s: 'Login' },
|
||
|
{ n: UPLOAD_FILES, s: 'Upload Files' },
|
||
|
{ n: DELETE_FILES, s: 'Delete Files' },
|
||
|
{ n: EDIT_FILES, s: 'Edit Files' },
|
||
|
{ n: CREATE_AND_DELETE_PLAYLISTS, s: 'Create/Delete Playlists' },
|
||
|
{ n: EDIT_PLAYLISTS, s: 'Edit Playlists' },
|
||
|
{ n: EDIT_INSTANCES, s: 'Edit Instances' },
|
||
|
{ n: EDIT_USERS, s: 'Edit Users' },
|
||
|
];
|
||
|
let uprivs = [];
|
||
|
privs.forEach(p => {
|
||
|
if (requirePrivileges(p.n)(client)) {
|
||
|
uprivs.push(p.s);
|
||
|
}
|
||
|
})
|
||
|
|
||
|
if (uprivs.length >= 1) {
|
||
|
reply(`You have the following privileges: ${uprivs.join(", ")}.`);
|
||
|
} else {
|
||
|
reply(`You have no privileges.`);
|
||
|
}
|
||
|
//reply(getUsersByClient(client).map(u => u.privileges().toString(2)).join(", "));
|
||
|
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('playing')
|
||
|
.help('Show what\'s currently playing')
|
||
|
.manual('Show what\'s currently playing')
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (engine.getBackend() !== 'discord' || !ev.message) {
|
||
|
if (!audio.isPlaying()) {
|
||
|
successReaction(ev, reply);
|
||
|
return reply('There is nothing playing at the moment.');
|
||
|
}
|
||
|
|
||
|
reply(formatTrack(media.getCurrentTrack()));
|
||
|
successReaction(ev, reply);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let msg = getPlayingEmbed();
|
||
|
if (!audio.isPlaying()) {
|
||
|
msg.content = 'There is nothing playing at the moment.';
|
||
|
}
|
||
|
|
||
|
backend.extended().createMessage(ev.message.channelID(), msg, (err, res) => {
|
||
|
if (err) return engine.log(err);
|
||
|
if (!res) return engine.log('Error: empty response');
|
||
|
|
||
|
const {id, channel_id} = JSON.parse(res);
|
||
|
|
||
|
// messages that should be deleted
|
||
|
let deleteMsg = [];
|
||
|
const msgId = ev.message ? ev.message.ID() : null;
|
||
|
const index = lastEmbeds.findIndex(embed => embed.channelId == channel_id);
|
||
|
if (index !== -1) {
|
||
|
if (config.deleteOldMessages) {
|
||
|
// delete previous embed
|
||
|
deleteMsg.push(lastEmbeds[index].messageId);
|
||
|
// delete previous command from user
|
||
|
if (lastEmbeds[index].messageId) {
|
||
|
deleteMsg.push(lastEmbeds[index].invokeMessageId);
|
||
|
}
|
||
|
}
|
||
|
// save new embed
|
||
|
lastEmbeds[index].messageId = id;
|
||
|
lastEmbeds[index].invokeMessageId = msgId;
|
||
|
} else {
|
||
|
// save new embed
|
||
|
lastEmbeds.push({
|
||
|
channelId: channel_id,
|
||
|
messageId: id,
|
||
|
invokeMessageId: msgId
|
||
|
});
|
||
|
}
|
||
|
|
||
|
deleteMessages(channel_id, deleteMsg);
|
||
|
|
||
|
if (HIDE_REACTIONS_IF_PRIVATE && ev.mode === 1) return;
|
||
|
|
||
|
wait(1000)
|
||
|
// create reaction controls
|
||
|
.then(() => createReaction(channel_id, id, REACTION_PREV))
|
||
|
.then(() => wait(150))
|
||
|
.then(() => createReaction(channel_id, id, REACTION_PLAYPAUSE))
|
||
|
.then(() => wait(150))
|
||
|
.then(() => createReaction(channel_id, id, REACTION_NEXT));
|
||
|
});
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('next')
|
||
|
.help('Play the next track')
|
||
|
.manual('Plays the next track (only when a playlist or queue is active).')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
media.playNext();
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('prev')
|
||
|
.alias('previous')
|
||
|
.help('Play the previous track')
|
||
|
.manual('Plays the previous track (only when a playlistis active).')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
media.playPrevious();
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('search')
|
||
|
.alias('s')
|
||
|
.addArgument(args => args.rest.setName('searchstring'))
|
||
|
.help('Search for tracks')
|
||
|
.manual('Searches for tracks, returns 20 results at most.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK, ENQUEUE))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no searchstring given
|
||
|
if (!args.searchstring) {
|
||
|
reply(USAGE_PREFIX + 'search <searchstring>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const tracks = media.search(args.searchstring);
|
||
|
if (tracks.length == 0) {
|
||
|
reply('Sorry, nothing found.');
|
||
|
successReaction(ev, reply);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const response = tracks.map(formatTrack).join("\n")
|
||
|
reply(response);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('play')
|
||
|
.alias('p')
|
||
|
.addArgument(args => args.rest.setName('idORsearchstring', 'searchstring / uuid'))
|
||
|
.help('Play a track by its id or name')
|
||
|
.manual('Plays a track by its id or searches for a track and plays the first match.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
let query = args.idORsearchstring;
|
||
|
// print syntax if no idORsearchstring given
|
||
|
if (!query) {
|
||
|
reply(USAGE_PREFIX + 'play <searchstring / uuid>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let track = media.getTrackByID(query);
|
||
|
if (!track) {
|
||
|
let tracks = media.search(query);
|
||
|
if (tracks.length > 0) {
|
||
|
track = tracks[0];
|
||
|
} else {
|
||
|
query = stripURL(query);
|
||
|
if (query.match(PATTERN_URL)) {
|
||
|
if (!query.match(PATTERN_YT_DOMAIN)) {
|
||
|
if (media.playURL(query)) {
|
||
|
successReaction(ev, reply);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
if (media.ytStream(query)) {
|
||
|
successReaction(ev, reply);
|
||
|
return;
|
||
|
}
|
||
|
const jobId = media.yt(query);
|
||
|
handleYT(jobId, ev, reply);
|
||
|
return;
|
||
|
}
|
||
|
return reply('Sorry, nothing found.');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
track.play();
|
||
|
reply(`Playing ${formatTrack(track)}`);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('playlist')
|
||
|
.addArgument(args => args.rest.setName('playlistname'))
|
||
|
.help('Start playing back the playlist <playlistname>')
|
||
|
.manual('starts playing back the playlist <playlistname>.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no playlistname given
|
||
|
if (!args.playlistname) {
|
||
|
reply(USAGE_PREFIX + 'playlist <playlistname>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const match = media.getPlaylists().find(playlist => {
|
||
|
// case insensitive equals
|
||
|
return playlist.name() == args.playlistname || playlist.name().localeCompare(args.playlistname, undefined, { sensitivity: 'accent' }) === 0
|
||
|
});
|
||
|
|
||
|
if (!match) {
|
||
|
reply('Sorry, no matching playlist found.');
|
||
|
successReaction(ev, reply);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
media.playlistPlayByID(match, 0);
|
||
|
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('queue')
|
||
|
.alias('q')
|
||
|
.addArgument(args => args.rest.setName('idORsearchstring', 'searchstring / uuid').optional(true))
|
||
|
.help('Enqueue a track or resume queue')
|
||
|
.manual('Enqueue a track by its id or search for a track and enqueue the first match. When no track is provided it wil resume the queue.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK, ENQUEUE))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (!args.idORsearchstring) {
|
||
|
if (!audio.isPlaying()) {
|
||
|
media.playQueueNext();
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let track = media.getTrackByID(args.idORsearchstring);
|
||
|
if (!track) {
|
||
|
const tracks = media.search(args.idORsearchstring);
|
||
|
if (tracks.length > 0) {
|
||
|
track = tracks[0];
|
||
|
} else {
|
||
|
reply('Sorry, nothing found.');
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
track.enqueue();
|
||
|
reply(`Added ${formatTrack(track)} to the queue`);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('queuenext')
|
||
|
.alias('qnext', 'qn')
|
||
|
.addArgument(args => args.rest.setName('idORsearchstring', 'searchstring / uuid'))
|
||
|
.help('Prepends a track to the queue')
|
||
|
.manual('Prepends a track by its id or searches for a track and prepends the first match to the queue.')
|
||
|
.checkPermission(requirePrivileges(SKIP_QUEUE))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no idORsearchstring given
|
||
|
if (!args.idORsearchstring) {
|
||
|
reply(USAGE_PREFIX + 'queuenext <searchstring / uuid>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let track = media.getTrackByID(args.idORsearchstring);
|
||
|
if (!track) {
|
||
|
const tracks = media.search(args.idORsearchstring);
|
||
|
if (tracks.length > 0) {
|
||
|
track = tracks[0];
|
||
|
} else {
|
||
|
reply('Sorry, nothing found.');
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
track.enqueue();
|
||
|
reply(`Added ${formatTrack(track)} to the queue`);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('stop')
|
||
|
.help('Stop playback')
|
||
|
.manual('Stops playback.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
media.stop();
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('!stop')
|
||
|
.help('Stop playback and remove idle-track')
|
||
|
.manual('Stops playback and removes idle-track.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK|EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
media.stop();
|
||
|
media.clearIdleTrack();
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('volume')
|
||
|
.alias('vol')
|
||
|
.addArgument(args => args.string.setName('value'))
|
||
|
.help('Change the volume')
|
||
|
.manual('Changes the volume.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
let value = args.value;
|
||
|
let volume = audio.getVolume();
|
||
|
|
||
|
switch (value) {
|
||
|
case 'up':
|
||
|
volume += 10;
|
||
|
break;
|
||
|
case 'dn':
|
||
|
case 'down':
|
||
|
volume -= 10;
|
||
|
break;
|
||
|
default:
|
||
|
value = parseInt(value, 10);
|
||
|
if (value >= 0 && value <= 100) {
|
||
|
volume = value;
|
||
|
} else {
|
||
|
reply(USAGE_PREFIX + 'volume <up|down|dn|0-100>');
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (volume < 0) {
|
||
|
volume = 0;
|
||
|
} else if (volume > 100) {
|
||
|
volume = 100;
|
||
|
}
|
||
|
|
||
|
audio.setVolume(volume);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('stream')
|
||
|
.addArgument(args => args.string.setName('url'))
|
||
|
.help('Stream a url')
|
||
|
.manual('Streams from <url>; this may be http-streams like shoutcast / icecast or just remote soundfiles.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no url given
|
||
|
if (!args.url) {
|
||
|
reply(USAGE_PREFIX + 'stream <url>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const url = stripURL(args.url);
|
||
|
|
||
|
if (url.match(PATTERN_YT_DOMAIN)) {
|
||
|
if (!media.ytStream(url)) {
|
||
|
reply(ERROR_PREFIX + 'Unable to stream this YouTube URL.');
|
||
|
return;
|
||
|
}
|
||
|
successReaction(ev, reply);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!media.playURL(url)) {
|
||
|
reply(ERROR_PREFIX + 'Unable to stream this URL.');
|
||
|
return;
|
||
|
}
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('say')
|
||
|
.addArgument(args => args.rest.setName('text'))
|
||
|
.help('Say a text via TTS')
|
||
|
.manual('Uses text-to-speech (if configured) to say the given text.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no text given
|
||
|
if (!args.text) {
|
||
|
reply(USAGE_PREFIX + 'say <text>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
audio.say(args.text);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('sayex')
|
||
|
.addArgument(args => args.string.setName('locale'))
|
||
|
.addArgument(args => args.rest.setName('text'))
|
||
|
.help('Say a text via TTS with given locale')
|
||
|
.manual('Uses text-to-speech (if configured) to say the given text with a given locale.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no locale/text given
|
||
|
if (!args.locale || !args.text) {
|
||
|
reply(USAGE_PREFIX + 'sayex <locale> <text>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
audio.say(args.text, args.locale);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('ttsurl')
|
||
|
.addArgument(args => args.string.setName('url'))
|
||
|
.help('Set the TTS url.')
|
||
|
.manual('Sets the TTS url.')
|
||
|
.checkPermission(requirePrivileges(EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no url given
|
||
|
if (!args.url) {
|
||
|
reply(USAGE_PREFIX + 'ttsurl <url>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
audio.setTTSURL(stripURL(args.url));
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('ttslocale')
|
||
|
.addArgument(args => args.string.setName('locale'))
|
||
|
.help('Set the TTS locale.')
|
||
|
.manual('Sets the TTS locale.')
|
||
|
.checkPermission(requirePrivileges(EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no locale given
|
||
|
if (!args.locale) {
|
||
|
reply(USAGE_PREFIX + 'ttslocale <locale>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
audio.setTTSDefaultLocale(args.locale);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('yt')
|
||
|
.addArgument(args => args.string.setName('url'))
|
||
|
.help('Play <url> via youtube-dl')
|
||
|
.manual('Plays <url> via external youtube-dl (if enabled); beware: the file will be downloaded first and played back afterwards, so there might be a slight delay before playback starts.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
const url = stripURL(args.url);
|
||
|
if (!url) return reply(USAGE_PREFIX + 'yt <url>');
|
||
|
if (!url.match(PATTERN_URL)) return reply(ERROR_PREFIX + 'Invalid URL.');
|
||
|
|
||
|
const jobId = media.yt(url);
|
||
|
ytCallback(jobId, (ytev, err) => {
|
||
|
if (err) {
|
||
|
// try to stream
|
||
|
if (!media.ytStream(url)) {
|
||
|
if (err.startsWith("exit status")) {
|
||
|
return reply(ERROR_PREFIX + `Error: ${err}; Please see "Upload" page in web-interface for more details.`);
|
||
|
}
|
||
|
return reply(ERROR_PREFIX + `Error: ${err}`);
|
||
|
}
|
||
|
}
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
createCommand('ytstream')
|
||
|
.alias('streamyt')
|
||
|
.addArgument(args => args.string.setName('url'))
|
||
|
.help('Stream <url> via youtube-dl')
|
||
|
.manual('Streams <url> via external youtube-dl (if enabled)')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
const url = stripURL(args.url);
|
||
|
if (!url) return reply(USAGE_PREFIX + 'ytstream <url>');
|
||
|
if (!url.match(PATTERN_URL)) return reply(ERROR_PREFIX + 'Invalid URL.');
|
||
|
|
||
|
if (!media.ytStream(url)) {
|
||
|
return reply(ERROR_PREFIX + 'Unable to stream this URL.');
|
||
|
}
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('ytdl')
|
||
|
.addArgument(args => args.string.setName('url'))
|
||
|
.help('Download and play <url> via youtube-dl')
|
||
|
.manual('Plays <url> via external youtube-dl (if enabled); beware: the file will be downloaded first and played back afterwards, so there might be a slight delay before playback starts; additionally, the file will be stored.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK|UPLOAD_FILES))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
const url = stripURL(args.url);
|
||
|
if (!url) return reply(USAGE_PREFIX + 'ytdl <url>');
|
||
|
if (!url.match(PATTERN_URL)) return reply(ERROR_PREFIX + 'Invalid URL.');
|
||
|
|
||
|
const jobId = media.ytdl(url, true);
|
||
|
handleYT(jobId, ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('qyt')
|
||
|
.addArgument(args => args.string.setName('url'))
|
||
|
.help('Enqueue <url> via youtube-dl')
|
||
|
.manual('Enqueues <url> via external youtube-dl (if enabled); beware: the file will be downloaded first and played back afterwards, so there might be a slight delay before playback starts.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK, ENQUEUE))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
const url = stripURL(args.url);
|
||
|
if (!url) return reply(USAGE_PREFIX + 'qyt <url>');
|
||
|
if (!url.match(PATTERN_URL)) return reply(ERROR_PREFIX + 'Invalid URL.');
|
||
|
|
||
|
const jobId = media.enqueueYt(url);
|
||
|
handleYT(jobId, ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('qytdl')
|
||
|
.addArgument(args => args.string.setName('url'))
|
||
|
.help('Download and enqueue <url> via youtube-dl')
|
||
|
.manual('Enqueues <url> via external youtube-dl (if enabled); beware: the file will be downloaded first and played back afterwards, so there might be a slight delay before playback starts; additionally, the file will be stored.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK|UPLOAD_FILES, ENQUEUE|UPLOAD_FILES))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
const url = stripURL(args.url);
|
||
|
if (!url) return reply(USAGE_PREFIX + 'qytdl <url>');
|
||
|
if (!url.match(PATTERN_URL)) return reply(ERROR_PREFIX + 'Invalid URL.');
|
||
|
|
||
|
const jobId = media.enqueueYtdl(url);
|
||
|
handleYT(jobId, ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('shuffle')
|
||
|
.help('Toggle shuffle')
|
||
|
.manual('Toggles shuffle.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
audio.setShuffle(!audio.isShuffle());
|
||
|
reply(SUCCESS_PREFIX + `Shuffle is now ${audio.isShuffle() ? 'en' : 'dis'}abled.`);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('repeat')
|
||
|
.help('Toggle repeat')
|
||
|
.manual('Toggles repeat.')
|
||
|
.checkPermission(requirePrivileges(PLAYBACK))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
audio.setRepeat(!audio.isRepeat());
|
||
|
reply(SUCCESS_PREFIX + `Repeat is now ${audio.isRepeat() ? 'en' : 'dis'}abled.`);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
if (engine.getBackend() == 'ts3') {
|
||
|
createCommand('sub')
|
||
|
.help('Subscribe to bot')
|
||
|
.manual('Subscribes to the bot. (subscription transfer-mode only)')
|
||
|
.checkPermission(() => engine.isSubscriptionMode())
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (!engine.isSubscriptionMode()) {
|
||
|
reply(ERROR_PREFIX + 'This command only works if Transmit-Mode is set to Subscription.');
|
||
|
return;
|
||
|
}
|
||
|
client.subscribe(true);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('unsub')
|
||
|
.help('Unsubscribe from bot')
|
||
|
.manual('Unsubscribes from the bot. (subscription transfer-mode only)')
|
||
|
.checkPermission(() => engine.isSubscriptionMode())
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (!engine.isSubscriptionMode()) {
|
||
|
reply(ERROR_PREFIX + 'This command only works if Transmit-Mode is set to Subscription.');
|
||
|
return;
|
||
|
}
|
||
|
client.subscribe(false);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('subchan')
|
||
|
.help('Add subscription for channel')
|
||
|
.manual('Adds subscription for the channel the user is currently in. (subscription transfer-mode only)')
|
||
|
.checkPermission(client => requirePrivileges(EDIT_BOT_SETTINGS)(client) && engine.isSubscriptionMode())
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (!engine.isSubscriptionMode()) {
|
||
|
reply(ERROR_PREFIX + 'This command only works if Transmit-Mode is set to Subscription.');
|
||
|
return;
|
||
|
}
|
||
|
client.getChannels()[0].subscribe(true);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('unsubchan')
|
||
|
.help('Remove subscription for channel')
|
||
|
.manual('Removes subscription for the channel the user is currently in. (subscription transfer-mode only)')
|
||
|
.checkPermission(client => requirePrivileges(EDIT_BOT_SETTINGS)(client) && engine.isSubscriptionMode())
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (!engine.isSubscriptionMode()) {
|
||
|
reply(ERROR_PREFIX + 'This command only works if Transmit-Mode is set to Subscription.');
|
||
|
return;
|
||
|
}
|
||
|
client.getChannels()[0].subscribe(false);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('mode')
|
||
|
.addArgument(args => args.string.setName('mode'))
|
||
|
.help('Change Transmit-Mode')
|
||
|
.manual('Changes Transmit-Mode; 0 = to channel, 1 = subscription mode')
|
||
|
.checkPermission(requirePrivileges(EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
let mode = args.mode;
|
||
|
if (typeof mode === 'string') {
|
||
|
mode = mode.toLowerCase();
|
||
|
}
|
||
|
|
||
|
switch (mode) {
|
||
|
case "0":
|
||
|
case "chan":
|
||
|
case "channel":
|
||
|
engine.setSubscriptionMode(false);
|
||
|
reply(SUCCESS_PREFIX + 'Transmit-Mode is now set to Channel (default).');
|
||
|
successReaction(ev, reply);
|
||
|
break;
|
||
|
case "1":
|
||
|
case "sub":
|
||
|
case "subscription":
|
||
|
engine.setSubscriptionMode(true);
|
||
|
reply(SUCCESS_PREFIX + 'Transmit-Mode is now set to Subscription.');
|
||
|
successReaction(ev, reply);
|
||
|
break;
|
||
|
default:
|
||
|
reply(`Transmit-Mode is currently set to ${engine.isSubscriptionMode() ? 'Subscription' : 'Channel (default)'}.\n` + USAGE_PREFIX + 'mode <0|chan(nel)|1|sub(scription)>');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
createCommand('registration')
|
||
|
.addArgument(args => args.string.setName('value'))
|
||
|
.help('Enable / disable user registration via chat')
|
||
|
.manual('Enables / disables user registration via chat. Value should be either `enable` or `disable`.')
|
||
|
.checkPermission(requirePrivileges(EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
switch (args.value) {
|
||
|
case "enable":
|
||
|
engine.enableRegistration();
|
||
|
reply(SUCCESS_PREFIX + 'Registration is now enabled.');
|
||
|
successReaction(ev, reply);
|
||
|
break;
|
||
|
case "disable":
|
||
|
engine.disableRegistration();
|
||
|
reply(SUCCESS_PREFIX + 'Registration is now disabled.');
|
||
|
successReaction(ev, reply);
|
||
|
break;
|
||
|
default:
|
||
|
reply(`Registration is currently ${engine.registrationEnabled() ? 'en' : 'dis'}abled.\n` + USAGE_PREFIX + 'registration <enable|disable>');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
createCommand('prefix')
|
||
|
.addArgument(args => args.string.setName('prefix'))
|
||
|
.help('Change command prefix')
|
||
|
.manual('Changes the prefix for all core commands to <new prefix>, default is "!".')
|
||
|
.checkPermission(requirePrivileges(EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
// print syntax if no prefix given
|
||
|
if (!args.prefix) {
|
||
|
reply(USAGE_PREFIX + 'prefix <new prefix>');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
engine.setCommandPrefix(args.prefix);
|
||
|
reply(SUCCESS_PREFIX + 'New prefix: ' + args.prefix);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('ping')
|
||
|
.help('responds with "PONG"')
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
reply(`PONG`);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('version')
|
||
|
.help('Show version')
|
||
|
.manual('Shows the SinusBot version.')
|
||
|
.checkPermission(requirePrivileges(EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
reply(`SinusBot v${engine.version()} on ${engine.os()}\nsinusbot-commands.js v${meta.version}\ncommand.js v${command.getVersion()}`);
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
createCommand('reload')
|
||
|
.help('Reload scripts')
|
||
|
.manual('Reloads scripts.\nNote: Adding new scripts requires a complete sinusbot restart.')
|
||
|
.checkPermission(requirePrivileges(EDIT_BOT_SETTINGS))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
reply('reloading...');
|
||
|
let success = engine.reloadScripts();
|
||
|
if (success) {
|
||
|
reply(SUCCESS_PREFIX + `Scripts reloaded.\n*Please note: adding new scripts requires a complete sinusbot restart.*`);
|
||
|
successReaction(ev, reply);
|
||
|
} else {
|
||
|
reply('Unable to reload scripts. Did you allow it in your `config.ini`?');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
createCommand('join')
|
||
|
.help('Move the SinusBot to your channel')
|
||
|
.manual('Moves the SinusBot into your channel.')
|
||
|
.checkPermission(requirePrivileges(START_STOP))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
var channel = client.getChannels()[0]
|
||
|
if (!channel) {
|
||
|
return reply(ERROR_PREFIX+'I\'m unable to join your channel :(');
|
||
|
}
|
||
|
|
||
|
if (!getBotClient()) {
|
||
|
return reply(ERROR_BOT_NULL);
|
||
|
}
|
||
|
bot.moveTo(channel);
|
||
|
engine.setDefaultChannelID(channel.id())
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
|
||
|
/* // currently not working due to a bug.
|
||
|
createCommand('disconnect')
|
||
|
.help('Disconnect the SinusBot')
|
||
|
.manual('Disconnects the SinusBot.')
|
||
|
.checkPermission(requirePrivileges(START_STOP))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
backend.disconnect()
|
||
|
});
|
||
|
// */
|
||
|
|
||
|
if (engine.getBackend() == 'discord') {
|
||
|
createCommand('leave')
|
||
|
.help('Disconnect the SinusBot')
|
||
|
.manual('Disconnects the SinusBot from the current voice channel.')
|
||
|
.checkPermission(requirePrivileges(START_STOP))
|
||
|
.exec((client, args, reply, ev) => {
|
||
|
if (!getBotClient()) {
|
||
|
return reply(ERROR_BOT_NULL);
|
||
|
}
|
||
|
|
||
|
bot.moveTo('');
|
||
|
engine.setDefaultChannelID('');
|
||
|
successReaction(ev, reply);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
} // END COMMANDS-ENABLED
|
||
|
|
||
|
// stores last bot client object for `getBotClient()`
|
||
|
let bot = backend.getBotClient();
|
||
|
|
||
|
/**
|
||
|
* Wrapper for `backend.getBotClient()` because it sometimes returns `null` -_-
|
||
|
* @returns {Client} Bot client
|
||
|
*/
|
||
|
function getBotClient() {
|
||
|
bot = backend.getBotClient() || bot;
|
||
|
return bot;
|
||
|
}
|
||
|
|
||
|
/********** !playing stuff for discord **********/
|
||
|
if (engine.getBackend() == 'discord') {
|
||
|
event.on('discord:MESSAGE_REACTION_ADD', ev => {
|
||
|
let ename = ev.emoji.name;
|
||
|
// remove 0xefb88f aka. "VARIATION SELECTOR-16" (no idea why discord puts that at the end)
|
||
|
if (ename.endsWith('\ufe0f')) {
|
||
|
ename = ename.slice(0, -1);
|
||
|
}
|
||
|
const emoji = ev.emoji.id ? `${ename}:${ev.emoji.id}` : ename;
|
||
|
|
||
|
// ignore reactions that are not controls
|
||
|
if (![REACTION_PREV, REACTION_PLAYPAUSE, REACTION_NEXT].includes(emoji)) return;
|
||
|
|
||
|
// ignore reactions from the bot itself
|
||
|
if (backend.getBotClientID().endsWith(ev.user_id)) return;
|
||
|
|
||
|
// get user via id
|
||
|
let client = backend.getClientByID(`${ev.guild_id || backend.getBotClientID().split('/')[0]}/${ev.user_id}`);
|
||
|
|
||
|
if (!client) {
|
||
|
engine.log(`playing controls: using workaround for client '${ev.user_id}'`);
|
||
|
// @ts-ignore: workaround
|
||
|
client = fakeClient(ev.guild_id || backend.getBotClientID().split('/')[0], ev.user_id, ev.channel_id);
|
||
|
}
|
||
|
|
||
|
// ignore if no matching user found or reaction from the bot itself
|
||
|
if (!client) return engine.log(`playing controls: could not find client '${ev.user_id}'`);
|
||
|
|
||
|
// ignore if reaction is from the bot itself
|
||
|
if (client.isSelf()) return;
|
||
|
|
||
|
let callback = () => {
|
||
|
// delete the rection
|
||
|
deleteUserReaction(ev.channel_id, ev.message_id, ev.user_id, emoji);
|
||
|
|
||
|
// check if user has the 'playback' permission
|
||
|
if (!requirePrivileges(PLAYBACK)(client)) {
|
||
|
engine.log(`${client.nick()} is missing playback permissions for reaction controls`);
|
||
|
client.chat(ERROR_PREFIX + 'You need the playback permission to use reaction controls');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const track = media.getCurrentTrack();
|
||
|
|
||
|
switch (emoji) {
|
||
|
case REACTION_PREV:
|
||
|
// ignore if nothing is playing
|
||
|
if (!audio.isPlaying()) return;
|
||
|
|
||
|
if (media.getQueue().length !== 0) {
|
||
|
// start from beginning if we're playing queue
|
||
|
audio.seek(0);
|
||
|
} else {
|
||
|
// try prev (doesn't work for queue or folder)
|
||
|
media.playPrevious();
|
||
|
|
||
|
// fallback: start from beginning if there is no previous track
|
||
|
if (!audio.isPlaying()) {
|
||
|
if (track) track.play();
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case REACTION_PLAYPAUSE:
|
||
|
if (audio.isPlaying()) {
|
||
|
media.stop();
|
||
|
} else {
|
||
|
if (!track) return;
|
||
|
const pos = audio.getTrackPosition();
|
||
|
|
||
|
if (pos && pos < (track.duration() - 1000 /* milliseconds */)) {
|
||
|
// continue playing at last pos
|
||
|
audio.setMute(true);
|
||
|
track.play();
|
||
|
audio.seek(pos);
|
||
|
audio.setMute(false);
|
||
|
} else {
|
||
|
// or start from beginning if it already ended
|
||
|
track.play();
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case REACTION_NEXT:
|
||
|
if (audio.isPlaying()) {
|
||
|
media.playNext();
|
||
|
} else {
|
||
|
// is something in queue?
|
||
|
if (media.getQueue().length !== 0) {
|
||
|
// resume queue
|
||
|
media.playQueueNext();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// was reaction added to previous response?
|
||
|
if (lastEmbeds.some(embed => embed.messageId == ev.message_id)) {
|
||
|
callback();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
getMessage(ev.channel_id, ev.message_id).then(msg => {
|
||
|
// was reaction added to previous response of the bot?
|
||
|
if (backend.getBotClientID().endsWith(msg.author.id)) {
|
||
|
callback();
|
||
|
}
|
||
|
})
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Called when track or it's info changes
|
||
|
* @param {Track} track
|
||
|
*/
|
||
|
const onChange = track => {
|
||
|
if (config.songInStatus) {
|
||
|
const prefix = '🎵 ';
|
||
|
const suffix = ' 🎵';
|
||
|
|
||
|
// set track info as status
|
||
|
backend.extended().setStatus({
|
||
|
game: {
|
||
|
name: prefix + formatTrack(track) + suffix,
|
||
|
type: 2, // => 0 (game), 1 (streaming), 2 (listening)
|
||
|
},
|
||
|
status: "online",
|
||
|
afk: false
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// update embeds
|
||
|
lastEmbeds.forEach(async embed => {
|
||
|
await editMessage(embed.channelId, embed.messageId, getPlayingEmbed()).then(() => wait(100));
|
||
|
});
|
||
|
};
|
||
|
|
||
|
event.on('track', onChange);
|
||
|
event.on('trackInfo', onChange);
|
||
|
event.on('trackEnd', () => {
|
||
|
if (!config.songInStatus) {
|
||
|
return;
|
||
|
}
|
||
|
if (getBotClient()) {
|
||
|
bot.setDescription('');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns embed for current track
|
||
|
*/
|
||
|
function getPlayingEmbed() {
|
||
|
let track = media.getCurrentTrack();
|
||
|
|
||
|
if (!track) {
|
||
|
return {
|
||
|
content: "",
|
||
|
embed: {},
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let album = track.album();
|
||
|
let duration = track.duration();
|
||
|
|
||
|
let fields = [];
|
||
|
fields.push({
|
||
|
name: "Duration",
|
||
|
value: duration ? timestamp(duration) : 'stream',
|
||
|
inline: true
|
||
|
});
|
||
|
if (album) {
|
||
|
fields.push({
|
||
|
name: "Album",
|
||
|
value: album,
|
||
|
inline: true
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
content: formatTrack(track),
|
||
|
embed: {
|
||
|
title: formatTrack(track),
|
||
|
url: sinusbotURL || null,
|
||
|
color: 0xe13438,
|
||
|
thumbnail: {
|
||
|
url: sinusbotURL && track.thumbnail() ? `${sinusbotURL}/cache/${track.thumbnail()}` : null
|
||
|
},
|
||
|
fields: fields,
|
||
|
footer: {
|
||
|
icon_url: "https://sinusbot.github.io/logo.png",
|
||
|
text: "SinusBot"
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a fake client object from IDs. (Discord only)
|
||
|
* @param {string} guild Guild ID
|
||
|
* @param {string} id User ID
|
||
|
* @param {string} channelid Channel ID
|
||
|
*/
|
||
|
function fakeClient(guild, id, channelid) {
|
||
|
const clid = `${guild}/${id}`
|
||
|
return {
|
||
|
chat: (/** @type {string} */ str) => {
|
||
|
backend.extended().createMessage(channelid, {
|
||
|
content: str
|
||
|
});
|
||
|
},
|
||
|
isSelf: () => false,
|
||
|
id: () => clid,
|
||
|
uid: () => clid,
|
||
|
uniqueId: () => clid,
|
||
|
uniqueID: () => clid,
|
||
|
DBID: () => clid,
|
||
|
databaseID: () => clid,
|
||
|
databaseId: () => clid,
|
||
|
type: () => 1,
|
||
|
getURL: () => `<@${id}>`,
|
||
|
name: () => `unknown (ID: ${id})`,
|
||
|
nick: () => `unknown (ID: ${id})`,
|
||
|
phoneticName: () => '',
|
||
|
description: () => '',
|
||
|
getServerGroups: () => [],
|
||
|
getChannelGroup: () => null,
|
||
|
getChannels: () => [],
|
||
|
getAudioChannel: () => null,
|
||
|
equals: (/** @type {Client} */ client) => {
|
||
|
const uid = client.uid().split("/")
|
||
|
if (uid.length === 2) {
|
||
|
return uid[2] === id
|
||
|
} else {
|
||
|
return client.uid() === clid
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/********** helper functions **********/
|
||
|
|
||
|
/**
|
||
|
* Returns the first user with a given UID.
|
||
|
*
|
||
|
* @param {string} uid UID of the client
|
||
|
* @returns {User} first user with given uid
|
||
|
*/
|
||
|
function getUserByUid(uid) {
|
||
|
return engine.getUsers().find(user => user.tsUid() === uid)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns alls users that match the clients UID and ServerGroups.
|
||
|
*
|
||
|
* @param {Client} client
|
||
|
* @returns {User[]} Users that match the clients UID and ServerGroups.
|
||
|
*/
|
||
|
function getUsersByClient(client) {
|
||
|
return engine.getUsers().filter(user =>
|
||
|
// does the UID match?
|
||
|
client.uid() == user.uid() ||
|
||
|
// does a group ID match?
|
||
|
client.getServerGroups().map(group => group.id()).includes(user.groupId()) ||
|
||
|
// group ID '-1' matches everyone
|
||
|
user.groupId() == '-1'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a function that checks if a given user has all of the required privileges.
|
||
|
* @param {...number} privileges If at least one privilege matches the returned function will return true.
|
||
|
*/
|
||
|
function requirePrivileges(...privileges) {
|
||
|
return (/** @type {Client} */ client) => {
|
||
|
// check if at least one user has the required privileges
|
||
|
return getUsersByClient(client).some(user => {
|
||
|
// check if at least one privilege is found
|
||
|
return privileges.some(priv => {
|
||
|
return ((user.privileges()|user.instancePrivileges()) & priv) === priv;
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// /**
|
||
|
// * Returns a formatted string from a track.
|
||
|
// *
|
||
|
// * @param {Track} track
|
||
|
// * @returns {string} formatted string
|
||
|
// */
|
||
|
// function formatTrackWithID(track) {
|
||
|
// return `${format.code(track.id())} ${formatTrack(track)}`;
|
||
|
// }
|
||
|
|
||
|
/**
|
||
|
* Returns a formatted string from a track.
|
||
|
*
|
||
|
* @param {Track} track
|
||
|
* @returns {string} formatted string
|
||
|
*/
|
||
|
function formatTrack(track) {
|
||
|
let title = track.tempTitle() || track.title();
|
||
|
let artist = track.tempArtist() || track.artist();
|
||
|
return artist ? `${artist} - ${title}` : title;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes TeamSpeaks URL bb-code or Discords < > from a given string.
|
||
|
*
|
||
|
* @param {string} str
|
||
|
* @returns {string} str without [URL] [/URL] and < >
|
||
|
*/
|
||
|
function stripURL(str) {
|
||
|
// don't handle non-strings, return as provided
|
||
|
if (typeof str !== 'string') return str;
|
||
|
|
||
|
// remove surrounding [URL] [/URL] tags
|
||
|
let match = str.match(/\[URL\](.+)\[\/URL\]/i);
|
||
|
if (match && match.length >= 2) {
|
||
|
return match[1];
|
||
|
}
|
||
|
|
||
|
// remove surrounding < >
|
||
|
match = str.match(/<(.+)>/);
|
||
|
if (match && match.length >= 2) {
|
||
|
return match[1];
|
||
|
}
|
||
|
|
||
|
// if nothing matches just return str
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a more human readable timestamp (hours:minutes:secods)
|
||
|
* @param {number} milliseconds
|
||
|
*/
|
||
|
function timestamp(milliseconds) {
|
||
|
const SECOND = 1000; //milliseconds
|
||
|
const MINUTE = 60 * SECOND;
|
||
|
const HOUR = 60 * MINUTE;
|
||
|
|
||
|
let seconds = Math.floor(milliseconds / SECOND);
|
||
|
let minutes = Math.floor(milliseconds / MINUTE);
|
||
|
let hours = Math.floor(milliseconds / HOUR);
|
||
|
|
||
|
minutes = minutes % (HOUR/MINUTE);
|
||
|
seconds = seconds % (MINUTE/SECOND);
|
||
|
|
||
|
let str = '';
|
||
|
|
||
|
if (hours !== 0) {
|
||
|
str += hours + ':';
|
||
|
if (minutes <= 9) {
|
||
|
str += '0';
|
||
|
}
|
||
|
}
|
||
|
str += minutes + ':';
|
||
|
if (seconds <= 9) {
|
||
|
str += '0';
|
||
|
}
|
||
|
str += seconds;
|
||
|
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @ignore
|
||
|
* @typedef MessageEvent
|
||
|
* @type {object}
|
||
|
* @property {Client} client
|
||
|
* @property {Channel} channel
|
||
|
* @property {string} text
|
||
|
* @property {number} mode
|
||
|
* @property {DiscordMessage} [message]
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Gives the user feedback if a command was successfull.
|
||
|
*
|
||
|
* @param {MessageEvent} ev
|
||
|
* @param {(msg: string) => void} reply
|
||
|
*/
|
||
|
function successReaction(ev, reply) {
|
||
|
if (!config.createSuccessReaction) {
|
||
|
return;
|
||
|
}
|
||
|
switch (engine.getBackend()) {
|
||
|
case "discord":
|
||
|
if (ev.message) ev.message.createReaction(REACTION_SUCCESS)
|
||
|
return
|
||
|
case "ts3":
|
||
|
return reply(`${REACTION_SUCCESS} done!`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Waits for given milliseconds.
|
||
|
* @param {number} ms Time to wait for in milliseconds.
|
||
|
* @return {Promise}
|
||
|
*/
|
||
|
function wait(ms) {
|
||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds a reaction to a message.
|
||
|
* @param {string} channelID Channel ID
|
||
|
* @param {string} messageID Message ID
|
||
|
* @param {string} emoji Emoji
|
||
|
* @return {Promise<object>}
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*/
|
||
|
function createReaction(channelID, messageID, emoji) {
|
||
|
return discord('PUT', `/channels/${channelID}/messages/${messageID}/reactions/${emoji}/@me`, null, false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes a reaction from a message.
|
||
|
* @param {string} channelID Channel ID
|
||
|
* @param {string} messageID Message ID
|
||
|
* @param {string} userID User ID
|
||
|
* @param {string} emoji Emoji
|
||
|
* @return {Promise<object>}
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*/
|
||
|
function deleteUserReaction(channelID, messageID, userID, emoji) {
|
||
|
return discord('DELETE', `/channels/${channelID}/messages/${messageID}/reactions/${emoji}/${userID}`, null, false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a message.
|
||
|
* @param {string} channelID Channel ID
|
||
|
* @param {string} messageID Message ID
|
||
|
* @return {Promise<object>}
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*/
|
||
|
function getMessage(channelID, messageID) {
|
||
|
return discord('GET', `/channels/${channelID}/messages/${messageID}`, null, true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Edits a message.
|
||
|
* @param {string} channelID Channel ID
|
||
|
* @param {string} messageID Message ID
|
||
|
* @param {object} message New message
|
||
|
* @return {Promise<object>}
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*/
|
||
|
function editMessage(channelID, messageID, message) {
|
||
|
return discord('PATCH', `/channels/${channelID}/messages/${messageID}`, message, true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deletes a message.
|
||
|
* @param {string} channelID Channel ID
|
||
|
* @param {string} messageID Message ID
|
||
|
* @return {Promise<object>}
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*/
|
||
|
function deleteMessage(channelID, messageID) {
|
||
|
return discord('DELETE', `/channels/${channelID}/messages/${messageID}`, null, false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deletes multiple messages.
|
||
|
* @param {string} channelID Channel ID
|
||
|
* @param {string[]} messageIDs Message IDs
|
||
|
* @return {Promise<object>}
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*/
|
||
|
function deleteMessages(channelID, messageIDs) {
|
||
|
switch (messageIDs.length) {
|
||
|
case 0: return Promise.resolve();
|
||
|
case 1: return deleteMessage(channelID, messageIDs[0]);
|
||
|
default: return discord('POST', `/channels/${channelID}/messages/bulk-delete`, {messages: messageIDs}, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Executes a discord API call
|
||
|
* @param {string} method http method
|
||
|
* @param {string} path path
|
||
|
* @param {object} [data] json data
|
||
|
* @param {boolean} [repsonse] `true` if you're expecting a json response, `false` otherwise
|
||
|
* @return {Promise<object>}
|
||
|
* @author Jonas Bögle
|
||
|
* @license MIT
|
||
|
*/
|
||
|
function discord(method, path, data=null, repsonse=true) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
backend.extended().rawCommand(method, path, data, (err, data) => {
|
||
|
if (err) return reject(err);
|
||
|
if (repsonse) {
|
||
|
let res;
|
||
|
try {
|
||
|
res = JSON.parse(data);
|
||
|
} catch (err) {
|
||
|
engine.log(`${method} ${path} failed`)
|
||
|
engine.log(`${data}`)
|
||
|
return reject(err);
|
||
|
}
|
||
|
|
||
|
if (res === undefined) {
|
||
|
engine.log(`${method} ${path} failed`)
|
||
|
engine.log(`${data}`)
|
||
|
return reject('Invalid Response');
|
||
|
}
|
||
|
|
||
|
return resolve(res);
|
||
|
}
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
})
|