viki.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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. // Define a function to execute a command
  8. function execute(command, callback)
  9. {
  10. exec(command, function(error, stdout, stderr)
  11. {
  12. callback(stdout);
  13. });
  14. };
  15. // Define a new function to search + replace a char in a string
  16. String.prototype.replaceAll = function(remove, replace)
  17. {
  18. var target = this;
  19. return target.split(remove).join(replace);
  20. };
  21. // Initialize the bot.
  22. const client = new Discord.Client();
  23. // this allows us to define a voice connection with global scope
  24. var connection;
  25. var dispatcher;
  26. // this is the playlist queue for music
  27. var playlist = new Queue();
  28. // Array of music classes users can call (artist, track, etc)
  29. const musicTypes = ['track', 'title', 'song', 'artist', 'album'];
  30. const musicTypesString = "track, title, song, artist, album";
  31. // Here we load the config.json file that contains our token and our prefix values.
  32. const config = require("./config.json");
  33. // config.token contains the bot's token
  34. // config.prefix contains the message prefix.
  35. client.on("ready", () =>
  36. {
  37. // This event will run if the bot starts, and logs in, successfully.
  38. console.log(`Bot has started, with ${client.users.size} users, in ${client.channels.size} channels of ${client.guilds.size} guilds.`);
  39. // Example of changing the bot's playing game to something useful. `client.user` is what the
  40. // docs refer to as the "ClientUser".
  41. client.user.setActivity("Taking Over Chicago");
  42. });
  43. client.on("guildCreate", guild =>
  44. {
  45. // This event triggers when the bot joins a guild.
  46. console.log(`New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`);
  47. client.user.setActivity(`Serving ${client.guilds.size} servers`);
  48. });
  49. client.on("guildDelete", guild => {
  50. // this event triggers when the bot is removed from a guild.
  51. console.log(`I have been removed from: ${guild.name} (id: ${guild.id})`);
  52. client.user.setActivity("Taking Over Chicago");
  53. });
  54. // This plays the next song in the queue, and logs that in the channel where it was requested.
  55. function play()
  56. {
  57. let nextSong = playlist.dequeue();
  58. dispatcher = connection.playFile(nextSong[0]);
  59. console.log(`Playing ${nextSong[2]}.`);
  60. nextSong[1].reply(`Playing ${nextSong[2]}.`);
  61. dispatcher.setVolume(0.2);
  62. dispatcher.setBitrate(96);
  63. dispatcher.on("end", reason =>
  64. {
  65. if (!(playlist.isEmpty()))
  66. {
  67. play();
  68. console.log(reason);
  69. }
  70. else
  71. {
  72. console.log("Playlist exhausted, music playback stopped.");
  73. }
  74. });
  75. }
  76. client.on("message", async message =>
  77. {
  78. // This event will run on every single message received, from any channel or DM.
  79. // Ignores bot msgs
  80. if (message.author.bot) return;
  81. // ignores if message isn't prefixed
  82. if (message.content.indexOf(config.prefix) !== 0) return;
  83. // Here we separate our "command" name, and our "arguments" for the command.
  84. // e.g. if we have the message "+say Is this the real life?" , we'll get the following:
  85. // command = say
  86. // args = ["Is", "this", "the", "real", "life?"]
  87. const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
  88. const command = args.shift().toLowerCase();
  89. // Let's go with a few common example commands! Feel free to delete or change those.
  90. if (command === "ping")
  91. {
  92. // Calculates ping between sending a message and editing it, giving a nice round-trip latency.
  93. // The second ping is an average latency between the bot and the websocket server (one-way, not round-trip)
  94. const m = await message.channel.send("Ping?");
  95. m.edit(`Pong! Latency is ${m.createdTimestamp - message.createdTimestamp}ms. API Latency is ${Math.round(client.ping)}ms`);
  96. }
  97. if (command === "say")
  98. {
  99. // makes the bot say something and delete the original message. As an example, it's open to anyone to use.
  100. // To get the "message" itself we join the `args` back into a string with spaces:
  101. const sayMessage = args.join(" ");
  102. // Then we delete the command message (sneaky, right?). The catch just ignores the error with a cute smiley thing.
  103. message.delete().catch(O_o=>{});
  104. // And we get the bot to say the thing:
  105. message.channel.send(sayMessage);
  106. }
  107. if (command === "kick") {
  108. // This command must be limited to mods and admins. In this example we just hardcode the role names.
  109. // Please read on Array.some() returns true if any element meets the passed condition (map-like)
  110. if (!message.member.roles.some(r=>["Administrator", "Moderator","Admin","Mod"].includes(r.name)) )
  111. return message.reply("Sorry, you don't have permissions to use this!");
  112. // Let's first check if we have a member and if we can kick them!
  113. // message.mentions.members is a collection of people that have been mentioned, as GuildMembers.
  114. // We can also support getting the member by ID, which would be args[0]
  115. let member = message.mentions.members.first() || message.guild.members.get(args[0]);
  116. if (!member)
  117. return message.reply("Please mention a valid member of this server");
  118. if (!member.kickable)
  119. return message.reply("I cannot kick this user! Do they have a higher role? Do I have kick permissions?");
  120. // slice(1) removes the first part, which here should be the user mention or ID
  121. // join(' ') takes all the various parts to make it a single string.
  122. let reason = args.slice(1).join(' ');
  123. if (!reason) reason = "No reason provided";
  124. // Now, time for a swift kick in the nuts!
  125. await member.kick(reason)
  126. .catch(error => message.reply(`Sorry ${message.author} I couldn't kick because of : ${error}`));
  127. message.reply(`${member.user.tag} has been kicked by ${message.author.tag} because: ${reason}`);
  128. }
  129. if (command === "ban") {
  130. // Most of this command is identical to kick, except that here we'll only let admins do it.
  131. // In the real world mods could ban too, but this is just an example, right? ;)
  132. if (!message.member.roles.some(r=>["Administrator"].includes(r.name)) )
  133. return message.reply("Sorry, you don't have permissions to use this!");
  134. let member = message.mentions.members.first();
  135. if (!member)
  136. return message.reply("Please mention a valid member of this server");
  137. if (!member.bannable)
  138. return message.reply("I cannot ban this user! Do they have a higher role? Do I have ban permissions?");
  139. let reason = args.slice(1).join(' ');
  140. if (!reason) reason = "No reason provided";
  141. await member.ban(reason)
  142. .catch(error => message.reply(`Sorry ${message.author} I couldn't ban because of : ${error}`));
  143. message.reply(`${member.user.tag} has been banned by ${message.author.tag} because: ${reason}`);
  144. }
  145. if (command === "join")
  146. {
  147. // This command moves the bot to the channel given as the first arg.
  148. // This is the passed channel
  149. const channel = args.join(' ');
  150. const channelVar = message.guild.channels.find("name", channel);
  151. // Checks if channel is valid.
  152. if (!(message.guild.channels.exists("name", channel) && channelVar.type === "voice"))
  153. return message.reply(`Sorry ${message.author}, that channel doesn't appear to exist.`);
  154. // Joins the channel
  155. channelVar.join().then(conn =>
  156. {
  157. connection = conn;
  158. console.log('Connected!');
  159. }).catch(console.error);
  160. }
  161. if (command === "leave")
  162. {
  163. // this command remove the bot from the channel passed
  164. const channel = args.join(' ');
  165. const channelVar = message.guild.channels.find("name", channel);
  166. if (!(message.guild.channels.exists("name", channel) && channelVar.type === "voice"))
  167. return message.reply(`Sorry ${message.author}, that channel doesn't appear to exist.`);
  168. channelVar.leave();
  169. }
  170. if (command === "addmusic") // adds songs to queue, starts playback if none already
  171. {
  172. var type = args[0];
  173. if (!(musicTypes.includes(type)))
  174. {
  175. return message.reply("Sorry, that is not a valid command. Please enter something from: " + musicTypesString);
  176. }
  177. if (type == 'song' || type == 'track') type = 'title'; // account for poor beets API
  178. args.splice(0, 1);
  179. const query = args.join(' '); // creates a single string of all args (the query)
  180. var path; // this will hold the filepaths from our query
  181. exec(`beet ls -p ${type}:${query} | wc -l`, function (error, stdout, stderr)
  182. {
  183. if (error)
  184. {
  185. return message.reply(`Sorry, I encountered an issue looking for that: ${error}`);
  186. }
  187. else if (stdout === '\n' || stdout === '' || stdout === undefined)
  188. {
  189. return message.reply(`There were no results that matched your search. Please give the type and name of your query (e.g. song songname, album albumname...)`);
  190. }
  191. else
  192. {
  193. exec(`beet ls -p ${type}:${query}`, function (error, stdout, stderr)
  194. {
  195. if (error)
  196. {
  197. return message.reply(`Sorry, I encountered an issue looking for that: ${error}`);
  198. }
  199. else
  200. {
  201. path = stdout.trim();
  202. path = path.split("\n"); // now an array of paths (with spaces)
  203. // for each song, get the path and readable info, send to queue
  204. for (var i = 0; i < path.length; i++)
  205. {
  206. let filepathRaw = path[i];
  207. path[i] = path[i].replaceAll(" ", "\\ ");
  208. path[i] = path[i].replaceAll("'", "\\'");
  209. path[i] = path[i].replaceAll("&", "\\\46");
  210. path[i] = path[i].replaceAll("(", "\\(");
  211. path[i] = path[i].replaceAll(")", "\\)");
  212. let filepath = path[i]; // path[i] descoped in callback
  213. exec(`beet ls ${path[i]}`, function (error, stdouts, stderr)
  214. {
  215. if (error)
  216. {
  217. return message.reply(`Sorry, I encountered an issue looking for song ${i}: ${error}`);
  218. }
  219. else
  220. {
  221. stdouts = stdouts.trim();
  222. playlist.enqueue([filepathRaw, message, stdouts]);
  223. // check if music is playing, if not start it
  224. if ((dispatcher === undefined || dispatcher.destroyed == true) && !(playlist.isEmpty()))
  225. {
  226. play();
  227. }
  228. }
  229. });
  230. }
  231. }
  232. });
  233. }
  234. let amt = stdout.trim();
  235. message.reply(`${amt} songs added!`);
  236. });
  237. }
  238. if (command === 'stop') // clears playlist, stops music
  239. {
  240. playlist.reset();
  241. dispatcher.end();
  242. console.log("Playback stopped, playlist cleared.")
  243. }
  244. if (command === 'next') // returns next song in playlist, or informs that there is none
  245. {
  246. if (playlist.isEmpty())
  247. {
  248. message.reply("The playlist is empty.");
  249. }
  250. else
  251. {
  252. const next = playlist.peek();
  253. message.reply(`Next song is: ${next[2]}.`);
  254. }
  255. }
  256. if (command === 'pause') // pauses the dispatcher if playing, or does nothing
  257. {
  258. dispatcher.pause();
  259. message.reply("Playback paused.");
  260. }
  261. if (command === 'resume') // resumes the dispatcher, or does nothing
  262. {
  263. dispatcher.resume();
  264. message.reply("Playback resumed.");
  265. }
  266. if (command === 'skip') // starts playing the next song in the queue if it exists
  267. {
  268. if (playlist.isEmpty())
  269. {
  270. message.reply("Sorry, the playlist is empty.");
  271. }
  272. else
  273. {
  274. function resolveEnd()
  275. {
  276. return new Promise((success, fail) =>
  277. {
  278. dispatcher.end();
  279. dispatcher.on("end", () =>
  280. {
  281. success('Track skipped!');
  282. });
  283. dispatcher.on("error", () =>
  284. {
  285. fail('Couldn\'t skip :(');
  286. });
  287. });
  288. }
  289. resolveEnd();
  290. }
  291. }
  292. });
  293. client.login(config.token);