viki.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. // Load up the discord.js library
  2. const Discord = require("discord.js");
  3. // Load up the shell command library
  4. var exec = require('child_process').exec;
  5. // Load up the queue library
  6. const Queue = require('./Queue.js');
  7. // Initialize the bot.
  8. const client = new Discord.Client();
  9. // this allows us to define a voice connection with global scope
  10. var connection;
  11. var dispatcher;
  12. // playlist-related globals
  13. var playlist = new Queue();
  14. var played = []
  15. var cursong;
  16. var repeatall = false;
  17. var repeatone = false;
  18. // Array of music classes users can call (artist, track, etc)
  19. const musicTypes = ['track', 'title', 'song', 'artist', 'album'];
  20. const musicTypesString = "track, title, song, artist, album";
  21. // Here we load the config.json file that contains our token and our prefix values.
  22. const config = require("./config.json");
  23. // Define a function to execute a command
  24. function execute(command, callback)
  25. {
  26. exec(command, function(error, stdout, stderr)
  27. {
  28. callback(stdout);
  29. });
  30. };
  31. // Define a new function to search + replace a char in a string
  32. String.prototype.replaceAll = function(remove, replace)
  33. {
  34. var target = this;
  35. return target.split(remove).join(replace);
  36. };
  37. // This plays the next song in the queue, and logs that in the channel where it was requested.
  38. function play()
  39. {
  40. var nextSong = cursong;
  41. // if we aren't repeating cursong, dequeue
  42. if (!repeatone && cursong)
  43. {
  44. played.push(cursong);
  45. nextSong = playlist.dequeue();
  46. }
  47. // if we set repeat, but had no songs played yet
  48. // we should dequeue
  49. else if (!nextSong)
  50. {
  51. nextSong = playlist.dequeue();
  52. }
  53. dispatcher = connection.play(nextSong[0]);
  54. console.log(`Playing ${nextSong[2]}.`);
  55. nextSong[1].channel.send(`Playing ${nextSong[2]}.`);
  56. dispatcher.setVolume(0.2);
  57. dispatcher.setBitrate(96);
  58. cursong = nextSong;
  59. var endHandler = function endHandler(reason)
  60. {
  61. if (!(playlist.isEmpty()))
  62. {
  63. play();
  64. }
  65. else
  66. {
  67. if (repeatall)
  68. {
  69. playlist.makeFilled(played);
  70. played = [];
  71. play();
  72. console.log("Repeat all encountered.");
  73. }
  74. else
  75. {
  76. console.log("Playlist exhausted, music playback stopped.");
  77. }
  78. }
  79. }
  80. // what to do if it ends
  81. dispatcher.on("finish", endHandler);
  82. //dispatcher.on("close", endHandler);
  83. }
  84. // riven stuff
  85. // Load up the request library
  86. var Request = require("request");
  87. var unrolledStats = new Map();
  88. var rolledStats = new Map();
  89. var cacheTime = 0;
  90. function updateRivens()
  91. {
  92. Request.get("http://n9e5v4d8.ssl.hwcdn.net/repos/weeklyRivensPC.json", (error, response, body) =>
  93. {
  94. if (error) return console.dir(error);
  95. var rivenArr = JSON.parse(body);
  96. for (var i = 0; i < rivenArr.length; i++)
  97. {
  98. var info = Object.assign({}, rivenArr[i]);
  99. delete info.itemType;
  100. delete info.compatibility;
  101. delete info.rerolled;
  102. // veiled rivens
  103. if (!(rivenArr[i].compatibility))
  104. {
  105. // set value in map to info
  106. unrolledStats.set(rivenArr[i].itemType.toUpperCase(), info);
  107. }
  108. else // weapon-specific, so check if rolled or unrolled
  109. {
  110. if (rivenArr[i].rerolled === true)
  111. {
  112. rolledStats.set(rivenArr[i].compatibility, info);
  113. }
  114. else
  115. {
  116. unrolledStats.set(rivenArr[i].compatibility, info);
  117. }
  118. }
  119. }
  120. });
  121. }
  122. updateRivens();
  123. cacheTime = new Date().getTime() / 1000;
  124. client.on("ready", () =>
  125. {
  126. // This event will run if the bot starts, and logs in, successfully.
  127. console.log(`Bot has started, with ${client.users.cache.size} users, in ${client.channels.cache.size} channels of ${client.guilds.cache.size} guilds.`);
  128. console.log(`users: ${Array.from(client.users.cache.values()).map(each => each.username)}`);
  129. // Example of changing the bot's playing game to something useful. `client.user` is what the
  130. // docs refer to as the "ClientUser".
  131. client.user.setActivity("Taking Over Chicago");
  132. });
  133. client.on("guildCreate", guild =>
  134. {
  135. // This event triggers when the bot joins a guild.
  136. console.log(`New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`);
  137. client.user.setActivity(`Serving ${client.guilds.cache.size} servers`);
  138. });
  139. client.on("guildDelete", guild =>
  140. {
  141. // this event triggers when the bot is removed from a guild.
  142. console.log(`I have been removed from: ${guild.name} (id: ${guild.id})`);
  143. client.user.setActivity("Taking Over Chicago");
  144. });
  145. client.on('message', async msg =>
  146. {
  147. // Ignores bot msgs
  148. if (msg.author.bot) return;
  149. // ignores if message isn't prefixed
  150. if (msg.content.indexOf(config.prefix) !== 0) return;
  151. // Here we separate our "command" name, and our "arguments" for the command.
  152. // e.g. if we have the message "+say Is this the real life?" , we'll get the following:
  153. // command = say
  154. // args = ["Is", "this", "the", "real", "life?"]
  155. const args = msg.content.slice(config.prefix.length).trim().split(/ +/g);
  156. const command = args.shift().toLowerCase();
  157. switch (command)
  158. {
  159. // respond w/ bot latency
  160. case 'ping':
  161. var m = await msg.channel.send('pong');
  162. m.edit(`Pong! Latency is ${m.createdTimestamp - msg.createdTimestamp}ms. API Latency is ${Math.round(client.ws.ping)}ms`);
  163. break;
  164. // join the specified void channel
  165. case 'join':
  166. var channelName = args.join(' ');
  167. var channel = msg.guild.channels.cache.find(each => each.name === channelName && each.type === "voice");
  168. if (channel)
  169. {
  170. channel.join().then(conn =>
  171. {
  172. connection = conn;
  173. }).catch(console.error);
  174. }
  175. else
  176. {
  177. msg.reply(`Sorry ${msg.author.username}, that channel doesn't appear to exist.`);
  178. }
  179. break;
  180. // leave the specified voice channel
  181. case 'leave':
  182. var channelName = args.join(' ');
  183. var channel = msg.guild.channels.cache.find(each => each.name === channelName && each.type === "voice");
  184. if (channel)
  185. {
  186. channel.leave();
  187. }
  188. else
  189. {
  190. msg.reply(`Sorry ${msg.author.username}, that channel doesn't appear to exist.`);
  191. }
  192. break;
  193. // return the price of a riven for the specified weapon
  194. case 'price':
  195. // parse args
  196. var type = args[0];
  197. args.splice(0, 1);
  198. var query = args.join(' ');
  199. query = query.toUpperCase();
  200. // check cache freshness
  201. delta = (new Date().getTime() / 1000) - cacheTime;
  202. if (delta > 3600) updateRivens();
  203. if (type == "rolled")
  204. {
  205. var result = rolledStats.get(query);
  206. if (!(result))
  207. {
  208. return msg.channel.send("Sorry, I couldn't find that weapon. Please check your message and try again.");
  209. }
  210. result = JSON.stringify(result, undefined, 2);
  211. return msg.channel.send(result);
  212. }
  213. else if (type == "unrolled")
  214. {
  215. var result = unrolledStats.get(query);
  216. if (!(result))
  217. {
  218. return msg.channel.send("Sorry, I couldn't find that weapon. Please check your message and try again.");
  219. }
  220. result = JSON.stringify(result, undefined, 2);
  221. return msg.channel.send(result);
  222. }
  223. else
  224. {
  225. return msg.channel.send("Sorry, please enter a command in the form: price unrolled/rolled [weapon_name]");
  226. }
  227. break;
  228. // searches the DB for songs matching the user query, and responds w/ that info in chat
  229. case 'search':
  230. if (!(config.whitelist.includes(msg.author.tag)))
  231. {
  232. return msg.channel.send("Sorry, you're not allowed to run this command. Please contact the server owner to acquire that permission.")
  233. }
  234. var type = args[0];
  235. if (!(musicTypes.includes(type)))
  236. {
  237. return msg.channel.send("Sorry, that is not a valid command. Please enter something from: " + musicTypesString);
  238. }
  239. if (type == 'song' || type == 'track') type = 'title'; // account for poor beets API
  240. args.splice(0, 1);
  241. var query = args.join(' '); // creates a single string of all args (the query)
  242. var path; // this will hold the filepaths from our query
  243. exec(`beet ls ${type}:${query} | wc -l`, function (error, stdout, stderr)
  244. {
  245. if (error)
  246. {
  247. return msg.channel.send(`Sorry, I encountered an issue looking for that: ${error}`);
  248. }
  249. else if (stdout === '\n' || stdout === '' || stdout === undefined)
  250. {
  251. return msg.channel.send('There were no results that matched your search. Please give the type and name of your query (e.g. song songname, album albumname...)');
  252. }
  253. else
  254. {
  255. var result = 'Results:\n';
  256. result += stdout.trim();
  257. msg.channel.send(result);
  258. }
  259. });
  260. break;
  261. // add the songs returned by the user query to the playlist, start if nothing playing
  262. case 'addmusic':
  263. if (!(config.whitelist.includes(msg.author.tag)))
  264. {
  265. return msg.channel.send("Sorry, you're not allowed to run this command. Please contact the server owner to acquire that permission.")
  266. }
  267. if (!connection)
  268. {
  269. return msg.channel.send("Please add me to a voice channel before adding music.")
  270. }
  271. var type = args[0];
  272. if (!(musicTypes.includes(type)))
  273. {
  274. return msg.channel.send("Sorry, that is not a valid command. Please enter something from: " + musicTypesString);
  275. }
  276. if (type == 'song' || type == 'track') type = 'title'; // account for poor beets API
  277. args.splice(0, 1);
  278. query = args.join(' '); // creates a single string of all args (the query)
  279. var path; // this will hold the filepaths from our query
  280. exec(`beet ls -p ${type}:${query} | wc -l`, function (error, stdout, stderr)
  281. {
  282. if (error)
  283. {
  284. return msg.channel.send(`Sorry, I encountered an issue looking for that: ${error}`);
  285. }
  286. else if (stdout === '\n' || stdout === '' || stdout === undefined)
  287. {
  288. return msg.channel.send(`There were no results that matched your search. Please give the type and name of your query (e.g. song songname, album albumname...)`);
  289. }
  290. else
  291. {
  292. exec(`beet ls -p ${type}:${query}`, function (error, stdout, stderr)
  293. {
  294. if (error)
  295. {
  296. return msg.channel.send(`Sorry, I encountered an issue looking for that: ${error}`);
  297. }
  298. else
  299. {
  300. path = stdout.trim();
  301. path = path.split("\n"); // now an array of paths (with spaces)
  302. // for each song, get the path and readable info, send to queue
  303. for (var i = 0; i < path.length; i++)
  304. {
  305. let filepathRaw = path[i];
  306. path[i] = path[i].replaceAll(" ", "\\ ");
  307. path[i] = path[i].replaceAll("'", "\\'");
  308. path[i] = path[i].replaceAll("&", "\\\46");
  309. path[i] = path[i].replaceAll("(", "\\(");
  310. path[i] = path[i].replaceAll(")", "\\)");
  311. let filepath = path[i]; // path[i] descoped in callback
  312. exec(`beet ls ${path[i]}`, function (error, stdouts, stderr)
  313. {
  314. if (error)
  315. {
  316. return msg.channel.send(`Sorry, I encountered an issue looking for song ${i}: ${error}`);
  317. }
  318. else
  319. {
  320. stdouts = stdouts.trim();
  321. playlist.enqueue([filepathRaw, msg, stdouts]);
  322. // check if music is playing, if not start it
  323. if ((!dispatcher || dispatcher.ended || dispatcher.destroyed || dispatcher.writableFinished || dispatcher.writableEnded) && !(playlist.isEmpty()))
  324. {
  325. play();
  326. }
  327. }
  328. });
  329. }
  330. }
  331. });
  332. }
  333. let amt = stdout.trim();
  334. msg.channel.send(`${amt} songs added!`);
  335. });
  336. break;
  337. // stops playback
  338. case 'stop':
  339. playlist.reset();
  340. repeatone = false;
  341. repeatall = false;
  342. played = [];
  343. dispatcher.end();
  344. console.log("Playback stopped, playlist cleared.");
  345. msg.channel.send("Playback stopped, playlist cleared.");
  346. break;
  347. // returns the next song in the playlist
  348. case 'next':
  349. if (playlist.isEmpty())
  350. {
  351. msg.channel.send("The playlist is empty.");
  352. }
  353. else
  354. {
  355. var next = playlist.peek();
  356. msg.channel.send(`Next song is: ${next[2]}.`);
  357. }
  358. break;
  359. // returns the last song played before the current one
  360. case 'previous':
  361. if (played.length <= 1)
  362. {
  363. msg.channel.send("No previous song.");
  364. }
  365. else
  366. {
  367. let temp = played.slice(-1).pop();
  368. msg.channel.send(`Previous song was: ${temp[2]}`);
  369. }
  370. break;
  371. // returns the playlist
  372. case 'playlist':
  373. if (playlist.isEmpty())
  374. {
  375. msg.channel.send("The playlist is empty.");
  376. }
  377. else
  378. {
  379. var list = playlist.read();
  380. var retstr = ""
  381. for (var i = 0; i < list.length; i++)
  382. {
  383. retstr += `Song #${i + 1} is: ${list[i][2]}.\n`;
  384. }
  385. msg.channel.send(retstr);
  386. }
  387. break;
  388. // pauses the playback
  389. case 'pause':
  390. dispatcher.pause(true);
  391. msg.channel.send("Playback paused.");
  392. break;
  393. // resumes the playback
  394. case 'resume':
  395. dispatcher.resume();
  396. msg.channel.send("Playback resumed.");
  397. break;
  398. // sets repeat behaviour
  399. case 'repeat':
  400. var param = args[0];
  401. if (param === 'one' && cursong)
  402. {
  403. repeatone = true; // causes play function to repeat current cursong
  404. repeatall = false;
  405. msg.channel.send(`Repeating ${cursong[2]}.`);
  406. }
  407. else if (param === 'all') // track playlist, and repeat whole thing once empty
  408. {
  409. repeatone = false;
  410. repeatall = true;
  411. msg.channel.send("Repeating playlist.");
  412. }
  413. else if (param === 'off') // resets repeat variables
  414. {
  415. repeatone = false;
  416. repeatall = false;
  417. msg.channel.send("Repeat off.");
  418. }
  419. else
  420. {
  421. msg.channel.send("There was nothing to repeat, or an invalid option was given. Valid options are one, all, and off.");
  422. }
  423. break;
  424. // skips to the next song
  425. case 'skip':
  426. if (playlist.isEmpty())
  427. {
  428. msg.channel.send("Sorry, the playlist is empty.");
  429. }
  430. else
  431. {
  432. function resolveEnd()
  433. {
  434. return new Promise((success, fail) =>
  435. {
  436. dispatcher.end();
  437. dispatcher.on("finish", () =>
  438. {
  439. success('Track skipped!');
  440. });
  441. dispatcher.on("error", () =>
  442. {
  443. fail('Couldn\'t skip :(');
  444. });
  445. });
  446. }
  447. resolveEnd();
  448. }
  449. break;
  450. // goes back 1 song (plays last song, adds cursong back to the queue)
  451. case 'back':
  452. if (played.length == 0)
  453. {
  454. msg.channel.send("Sorry, there is no song to skip back to.");
  455. }
  456. else
  457. {
  458. function resolveEnd()
  459. {
  460. return new Promise((success, fail) =>
  461. {
  462. playlist.insert(cursong, 0); // put cursong back on the front
  463. let tempsong = played[played.length - 1]; // captures the song to go back to
  464. played = played.splice(played.length - 1, 1); // removes the last song from played
  465. playlist.insert(tempsong, 0); // put old song on the front
  466. dispatcher.end(); // stop playing wrong song
  467. dispatcher.on("finish", () =>
  468. {
  469. success('Track reversed!');
  470. });
  471. dispatcher.on("error", () =>
  472. {
  473. fail('Couldn\'t skip :(');
  474. });
  475. });
  476. }
  477. resolveEnd();
  478. }
  479. break;
  480. // respond w/ error if command not recognized
  481. default:
  482. msg.channel.send("Sorry, that command isn't recognized.");
  483. }
  484. });
  485. client.login(config.token);