From a980ace51ecb9751d1a36b116ef6a4d7cd039964 Mon Sep 17 00:00:00 2001 From: Julius de Jeu Date: Mon, 15 Oct 2018 22:25:51 +0200 Subject: [PATCH] Add custom command class --- src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt | 28 ++-- src/main/kotlin/nl/voidcorp/dbot/Util.kt | 9 +- .../nl/voidcorp/dbot/commands/Commands.kt | 23 ++-- .../kotlin/nl/voidcorp/dbot/commands/Music.kt | 126 ++++++++++-------- .../nl/voidcorp/dbot/commands/UnityCommand.kt | 75 +++++++++++ .../nl/voidcorp/dbot/music/TrackScheduler.kt | 69 ++++++++-- 6 files changed, 238 insertions(+), 92 deletions(-) create mode 100644 src/main/kotlin/nl/voidcorp/dbot/commands/UnityCommand.kt diff --git a/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt b/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt index 7033254..74efed1 100644 --- a/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt +++ b/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt @@ -1,8 +1,7 @@ package nl.voidcorp.dbot -import com.jagrosh.jdautilities.command.CommandClient +import com.jagrosh.jdautilities.command.Command import com.jagrosh.jdautilities.command.CommandClientBuilder -import com.jagrosh.jdautilities.examples.command.PingCommand import com.sedmelluq.discord.lavaplayer.jdaudp.NativeAudioSendFactory import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager @@ -20,6 +19,7 @@ import net.dv8tion.jda.webhook.WebhookClient import net.dv8tion.jda.webhook.WebhookClientBuilder import nl.voidcorp.dbot.commands.helloCommand import nl.voidcorp.dbot.commands.initMusic +import nl.voidcorp.dbot.commands.pingCommand import nl.voidcorp.dbot.storage.GitlabWebhook import org.slf4j.LoggerFactory import java.io.File @@ -31,6 +31,8 @@ val cb = CommandClientBuilder() val log = LoggerFactory.getLogger("UnityBot") +val commands = mutableListOf() + @Suppress("JoinDeclarationAndAssignment") fun main(args: Array) { @@ -88,31 +90,33 @@ fun main(args: Array) { ctx.status(200).result("OK") } - lateinit var client: CommandClient cb.setOwnerId("168743656738521088") cb.setPrefix("!") cb.setAlternativePrefix("+") - cb.addCommand(PingCommand()) - cb.addCommand(helloCommand) + commands.addAll(helloCommand, pingCommand) cb.setGame(Game.watching("fraud and \uD83C\uDFB5")) + val replies = listOf("Hello %%", "Why hello there %%!", "Hello there %%", "General %%. You are a bold one", "A wild %% appeared!") cb.setHelpConsumer { event -> - event.reply(MessageBuilder("Help for ${event.selfMember.asMention}\n").append { + val greeting = replies.random().replace("%%", event.member.effectiveName) + val mb = MessageBuilder("$greeting\n").append { var st = "" - for (c in client.commands) { + for (c in commands) { st += "`${c.name}`: ${c.help}${if (c.aliases.isNotEmpty()) " (alias: `${c.aliases.first()}`)" else ""}\n" } st - }.build() - ) + } + + + event.reply(mb.build()) } initMusic() - - client = cb.build() - + cb.addCommands(*commands.toTypedArray()) + val client = cb.build() bot = JDABuilder(args[0]).addEventListener(client).addEventListener(nl.voidcorp.dbot.Events).setAudioSendFactory(NativeAudioSendFactory()).build() } lateinit var bot: JDA + diff --git a/src/main/kotlin/nl/voidcorp/dbot/Util.kt b/src/main/kotlin/nl/voidcorp/dbot/Util.kt index 97c3bf8..e1ad9cb 100644 --- a/src/main/kotlin/nl/voidcorp/dbot/Util.kt +++ b/src/main/kotlin/nl/voidcorp/dbot/Util.kt @@ -1,8 +1,13 @@ package nl.voidcorp.dbot +import com.google.gson.Gson import com.google.gson.GsonBuilder import java.util.* -val gson = GsonBuilder().setPrettyPrinting().create() +val gson: Gson = GsonBuilder().setPrettyPrinting().create() -val random = Random() \ No newline at end of file +val random = Random() + +fun MutableList.addAll(vararg e: E) = this.addAll(listOf(*e)) + +fun List.random(): E = this[random.nextInt(this.size)] \ No newline at end of file diff --git a/src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt b/src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt index a3ada11..2c97870 100644 --- a/src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt +++ b/src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt @@ -1,16 +1,19 @@ package nl.voidcorp.dbot.commands import com.jagrosh.jdautilities.command.CommandBuilder -import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException -import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist -import com.sedmelluq.discord.lavaplayer.track.AudioTrack import net.dv8tion.jda.core.MessageBuilder -import nl.voidcorp.dbot.playerManager +import java.time.temporal.ChronoUnit -val helloCommand = CommandBuilder().setName("hello").setHelp("Say hello to Andy!") - .setGuildOnly(false) - .build { event -> - event.reply(MessageBuilder("Hello, ").append(event.author).build()) - } + +val helloCommand = UnityCommand("hello", "Say hello to Andy!", aliases = *arrayOf()) { + it.reply(MessageBuilder("Hello, ").append(it.author).append("!").build()) +} + + +val pingCommand = UnityCommand("ping", help = "Check the bot's ping", aliases = *arrayOf("pong")) { event -> + event.reply("Ping: ...") { m -> + val ping = event.message.creationTime.until(m.creationTime, ChronoUnit.MILLIS) + m.editMessage("Ping: " + ping + "ms | Websocket: " + event.getJDA().getPing() + "ms").queue() + } +} diff --git a/src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt b/src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt index 6802bf4..6094617 100644 --- a/src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt +++ b/src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt @@ -5,20 +5,16 @@ import com.jagrosh.jdautilities.command.CommandEvent import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler import com.sedmelluq.discord.lavaplayer.player.AudioPlayer import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers -import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager -import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioTrack import com.sedmelluq.discord.lavaplayer.tools.FriendlyException import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame -import net.dv8tion.jda.core.EmbedBuilder import net.dv8tion.jda.core.audio.AudioSendHandler -import nl.voidcorp.dbot.cb +import nl.voidcorp.dbot.addAll +import nl.voidcorp.dbot.commands import nl.voidcorp.dbot.log import nl.voidcorp.dbot.music.TrackScheduler import nl.voidcorp.dbot.playerManager -import java.awt.Color -import java.time.LocalDateTime val guildMusicMap = mutableMapOf() @@ -27,8 +23,15 @@ fun initMusic() { AudioSourceManagers.registerRemoteSources(playerManager) - val queueCommand = CommandBuilder().setName("queue").addAlias("q").setHelp("Queue's a song").build { event -> + val queueCommand = CommandBuilder().setName("queue").addAlias("q").setHelp("Queue's a song (use without arguments to get the queue)").build { event -> + if (event.args.isEmpty()) { + if (!guildMusicMap.containsKey(event.guild.idLong) || (guildMusicMap[event.guild.idLong]!!.player.playingTrack == null)) + event.reply("The track list is empty") else + event.reply(guildMusicMap[event.guild.idLong]!!.getTrackList(event.member)) + + return@build + } val scheduler = if (guildMusicMap.containsKey(event.guild.idLong)) guildMusicMap[event.guild.idLong]!! else { val channel = event.guild.voiceChannels.firstOrNull { it.members.contains(event.member) } @@ -42,7 +45,6 @@ fun initMusic() { } - playerManager.loadItem(event.args, object : AudioLoadResultHandler { override fun loadFailed(exception: FriendlyException) { event.reply("Shit's fucked!") @@ -71,19 +73,7 @@ fun initMusic() { queueCommand.run(event) } - val soundcloudCommand = CommandBuilder().setName("soundcloud").addAlias("sc").setHelp("Play a song from SoundCloud!").build { event -> - val scheduler = if (guildMusicMap.containsKey(event.guild.idLong)) guildMusicMap[event.guild.idLong]!! else { - val channel = event.guild.voiceChannels.firstOrNull { it.members.contains(event.member) } - if (channel == null) { - event.reply("Join a voice Channel please!") - return@build - } - val s = TrackScheduler(playerManager.createPlayer(), event.guild, channel) - guildMusicMap[event.guild.idLong] = s - s - } - - + val soundcloudCommand = UnityMusicCommand("soundcloud", help = "Play a song via SoundCloud!", aliases = *arrayOf("sc")) { event, scheduler -> playerManager.loadItem(event.args, object : AudioLoadResultHandler { override fun loadFailed(exception: FriendlyException) { @@ -109,17 +99,7 @@ fun initMusic() { }) } - val playCommand = CommandBuilder().setName("play").addAlias("p").setHelp("Plays the song instantly").build { event -> - val scheduler = if (guildMusicMap.containsKey(event.guild.idLong)) guildMusicMap[event.guild.idLong]!! else { - val channel = event.guild.voiceChannels.firstOrNull { it.members.contains(event.member) } - if (channel == null) { - event.reply("Join a voice Channel please!") - return@build - } - val s = TrackScheduler(playerManager.createPlayer(), event.guild, channel) - guildMusicMap[event.guild.idLong] = s - s - } + val playCommand = UnityMusicCommand("play", "Force this song to be the next!", aliases = *arrayOf("p")) { event, scheduler -> playerManager.loadItem(event.args, object : AudioLoadResultHandler { override fun loadFailed(exception: FriendlyException) { @@ -146,18 +126,70 @@ fun initMusic() { } - val skipCommand = CommandBuilder().setName("skip").addAlias("s").setHelp("Skips the currently playing track").build { event -> + val skipCommand = UnityCommand("skip", help = "Skip the current song", aliases = *arrayOf("s"), category = musicCategory) { event -> val scheduler = guildMusicMap[event.guild.idLong] if (scheduler == null) { event.reply("There is no music playing?") } else { scheduler.skip() } + } + val npCommand = UnityMusicCommand("nowplaying", aliases = *arrayOf("np"), help = "Show the currently playing song") { event, scheduler -> + if (!scheduler.isSongPlaying()) { + event.reply("There is no song playing?") + } else { + event.reply(scheduler.getCurrentTrackInfo()) + } } - cb.addCommands(playCommand, skipCommand, queueCommand, ytCommand, soundcloudCommand) + val attachmentPlay = CommandBuilder().setName("attachment").addAlias("attach").setHelp("Plays the attached file (`use the optional comment to activate the command`)").build { event -> + val attach = event.message.attachments.firstOrNull() + if (attach == null) { + event.reply("I can't play an attachment without an attachment...") + } else { + val scheduler = if (guildMusicMap.containsKey(event.guild.idLong)) guildMusicMap[event.guild.idLong]!! else { + val channel = event.guild.voiceChannels.firstOrNull { it.members.contains(event.member) } + if (channel == null) { + event.reply("Join a voice Channel please!") + return@build + } + val s = TrackScheduler(playerManager.createPlayer(), event.guild, channel) + guildMusicMap[event.guild.idLong] = s + s + } + + playerManager.loadItem(attach.url, object : AudioLoadResultHandler { + override fun loadFailed(exception: FriendlyException) { + event.reply("Shit's fucked!") + } + + override fun trackLoaded(track: AudioTrack) { + scheduler.queue(track, event.member) + if (!scheduler.isQueueEmpty() or scheduler.isSongPlaying()) { + scheduler.getTrackInfo(track) + } + } + + override fun noMatches() { + } + + override fun playlistLoaded(playlist: AudioPlaylist) { + for (t in playlist.tracks) { + scheduler.queue(t, event.member) + } + + + } + + }) + + } + } + + + commands.addAll(playCommand, skipCommand, queueCommand, ytCommand, soundcloudCommand, npCommand, attachmentPlay) } fun getLinkFromSearch(event: CommandEvent, scheduler: TrackScheduler, shouldInsertFront: Boolean, searchPrefix: String = "ytsearch") { @@ -196,38 +228,20 @@ fun getLinkFromSearch(event: CommandEvent, scheduler: TrackScheduler, shouldInse else { val track = playlist.tracks.first() - if (!scheduler.isQueueEmpty() or scheduler.isSongPlaying()) { - if (track is SoundCloudAudioTrack) { - val scsm = track.sourceManager as SoundCloudAudioSourceManager - scsm.updateClientId() - val art = khttp.get("http://api.soundcloud.com/tracks/${track.info.identifier}?client_id=${scsm.clientId}", - headers = mapOf("Content-Type" to "application/json")).jsonObject["artwork_url"].toString().replace("large", "t300x300") - event.reply(EmbedBuilder() - .setFooter("Requested by ${event.member.effectiveName}", event.member.user.effectiveAvatarUrl) - .setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) - .setThumbnail(art) - .setTimestamp(LocalDateTime.now()).setColor(Color.decode("#ff8800")).build()) - } else { - event.reply(EmbedBuilder() - .setFooter("Requested by ${event.member.effectiveName}", event.member.user.effectiveAvatarUrl) - .setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) - .setThumbnail("https://img.youtube.com/vi/${track.info.identifier}/hqdefault.jpg") - .setTimestamp(LocalDateTime.now()).setColor(Color.decode("#ff0000")).build()) - } - - } if (shouldInsertFront) scheduler.insertFront(track, event.member) else scheduler.queue(track, event.member) + if (!scheduler.isQueueEmpty() or scheduler.isSongPlaying()) { + if (scheduler.player.playingTrack != track) + event.reply(scheduler.getTrackInfo(track)) + } } - - } }) diff --git a/src/main/kotlin/nl/voidcorp/dbot/commands/UnityCommand.kt b/src/main/kotlin/nl/voidcorp/dbot/commands/UnityCommand.kt new file mode 100644 index 0000000..5471613 --- /dev/null +++ b/src/main/kotlin/nl/voidcorp/dbot/commands/UnityCommand.kt @@ -0,0 +1,75 @@ +package nl.voidcorp.dbot.commands + +import com.jagrosh.jdautilities.command.Command +import com.jagrosh.jdautilities.command.CommandEvent +import nl.voidcorp.dbot.music.TrackScheduler +import nl.voidcorp.dbot.playerManager + +val musicCategory = Command.Category("Music Commands") { + val botExists = it.guild.getTextChannelsByName("bot", true).firstOrNull() != null + val musicExists = it.guild.getTextChannelsByName("music", true).firstOrNull() != null + val musicBotExists = it.guild.getTextChannelsByName("music-bot", true).firstOrNull() != null + when { + musicBotExists -> { + val res = it.textChannel.name == "music-bot" + + if (!res) it.reply("Music commands are only supported in the ${it.guild.getTextChannelsByName("music-bot", true).first().asMention} channel!") + res + } + musicExists -> { + val res = it.textChannel.name == "music" + if (!res) it.reply("Music commands are only supported in the ${it.guild.getTextChannelsByName("music", true).first().asMention} channel!") + res + } + botExists -> { + val res = it.textChannel.name == "bot" + if (!res) it.reply("Music commands are only supported in the ${it.guild.getTextChannelsByName("bot", true).first().asMention} channel!") + res + } + else -> true + } + +} + +val generalCategory = Command.Category("General commands") { + if (it.guild.getTextChannelsByName("bot", true).firstOrNull() == null) return@Category true + val test = it.textChannel.name.contains("bot") + if (!test) { + it.reply("Non music commands can only be used in the ${it.guild.getTextChannelsByName("bot", true).first().asMention} channel") + } + + test + +} + +open class UnityCommand(name: String, help: String = "", + category: Category = generalCategory, + arguments: String = "", vararg aliases: String = arrayOf(name.first().toString()), val exec: (event: CommandEvent) -> Unit) : Command() { + init { + super.name = name + super.help = help + super.category = category + super.arguments = arguments + super.aliases = aliases + } + + override fun execute(event: CommandEvent) = exec(event) +} + +class UnityMusicCommand(name: String, help: String = "", + category: Category = musicCategory, + arguments: String = "", vararg aliases: String = arrayOf(name.first().toString()), val mExec: (event: CommandEvent, scheduler: TrackScheduler) -> Unit) : UnityCommand(name, help, category, arguments, *aliases, exec = {}) { + override fun execute(event: CommandEvent) { + val scheduler = if (guildMusicMap.containsKey(event.guild.idLong)) guildMusicMap[event.guild.idLong]!! else { + val channel = event.guild.voiceChannels.firstOrNull { it.members.contains(event.member) } + if (channel == null) { + event.reply("Join a voice Channel please!") + return + } + val s = TrackScheduler(playerManager.createPlayer(), event.guild, channel) + guildMusicMap[event.guild.idLong] = s + s + } + mExec(event, scheduler) + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt b/src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt index 3f6893a..0f0d109 100644 --- a/src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt +++ b/src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt @@ -2,16 +2,14 @@ package nl.voidcorp.dbot.music import com.sedmelluq.discord.lavaplayer.player.AudioPlayer import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter +import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioTrack import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioTrack import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrack import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason import net.dv8tion.jda.core.EmbedBuilder -import net.dv8tion.jda.core.entities.Guild -import net.dv8tion.jda.core.entities.Member -import net.dv8tion.jda.core.entities.TextChannel -import net.dv8tion.jda.core.entities.VoiceChannel +import net.dv8tion.jda.core.entities.* import nl.voidcorp.dbot.commands.AudioPlayerSendHandler import nl.voidcorp.dbot.commands.guildMusicMap import nl.voidcorp.dbot.log @@ -19,6 +17,8 @@ import java.awt.Color import java.time.LocalDateTime import java.util.* import kotlin.concurrent.thread +import kotlin.concurrent.timer +import kotlin.concurrent.timerTask class TrackScheduler(val player: AudioPlayer, val guild: Guild, channel: VoiceChannel) : AudioEventAdapter() { @@ -54,13 +54,10 @@ class TrackScheduler(val player: AudioPlayer, val guild: Guild, channel: VoiceCh } override fun onTrackStart(player: AudioPlayer, track: AudioTrack) { - if (track is YoutubeAudioTrack) { + musicChannel.sendMessage(getCurrentTrackInfo()).append("Now playing").queue() + + /*if (track is YoutubeAudioTrack) { if (track.userData is Member) - musicChannel.sendMessage(EmbedBuilder() - .setFooter("Requested by ${(track.userData as Member).effectiveName}", (track.userData as Member) - .user.effectiveAvatarUrl).setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) - .setThumbnail("https://img.youtube.com/vi/${track.info.identifier}/hqdefault.jpg") - .setTimestamp(LocalDateTime.now()).setColor(Color.decode("#ff0000")).build()).append("Now playing").queue() } else if (track is SoundCloudAudioTrack) { val scsm = track.sourceManager as SoundCloudAudioSourceManager scsm.updateClientId() @@ -75,7 +72,7 @@ class TrackScheduler(val player: AudioPlayer, val guild: Guild, channel: VoiceCh } - +*/ } @@ -122,9 +119,57 @@ class TrackScheduler(val player: AudioPlayer, val guild: Guild, channel: VoiceCh if (endReason != AudioTrackEndReason.REPLACED) { guildMusicMap.remove(guild.idLong) musicChannel.sendMessage(EmbedBuilder().setColor(musicChannel.guild.selfMember.color).setTitle("I'm done here").setDescription("There are no more songs left in the queue.").setTimestamp(LocalDateTime.now()).build()).queue() - thread { guild.audioManager.closeAudioConnection() } + + Timer().schedule(timerTask { + guild.audioManager.closeAudioConnection() + }, 2000) + } } + fun getCurrentTrackInfo(): MessageEmbed = getTrackInfo(player.playingTrack) + + fun getTrackInfo(track: AudioTrack): MessageEmbed { + if (track is YoutubeAudioTrack) { + if (track.userData is Member) + return EmbedBuilder() + .setFooter("Requested by ${(track.userData as Member).effectiveName}", (track.userData as Member) + .user.effectiveAvatarUrl).setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) + .setThumbnail("https://img.youtube.com/vi/${track.info.identifier}/hqdefault.jpg") + .setTimestamp(LocalDateTime.now()).setColor(Color.decode("#ff0000")).build() + } else if (track is SoundCloudAudioTrack) { + val scsm = track.sourceManager as SoundCloudAudioSourceManager + scsm.updateClientId() + val art = khttp.get("http://api.soundcloud.com/tracks/${track.info.identifier}?client_id=${scsm.clientId}", headers = mapOf("Content-Type" to "application/json")).jsonObject["artwork_url"].toString().replace("large", "t300x300") + + if (track.userData is Member) + return EmbedBuilder() + .setFooter("Requested by ${(track.userData as Member).effectiveName}", (track.userData as Member) + .user.effectiveAvatarUrl).setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) + .setThumbnail(art) + .setTimestamp(LocalDateTime.now()).setColor(Color.decode("#ff5500")).build() + + }else if (track is HttpAudioTrack){ + if (track.userData is Member) + return EmbedBuilder() + .setFooter("Requested by ${(track.userData as Member).effectiveName}", (track.userData as Member) + .user.effectiveAvatarUrl).setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) + .setTimestamp(LocalDateTime.now()).setColor(Color.decode("#005A9C")).build() + } + + return EmbedBuilder() + .setFooter("Requested by ${(track.userData as Member).effectiveName}", (track.userData as Member) + .user.effectiveAvatarUrl).setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) + .setTimestamp(LocalDateTime.now()).setColor(musicChannel.guild.selfMember.color).build() + } + + fun getTrackList(member: Member): MessageEmbed { + return EmbedBuilder().setTitle("Hey ${member.effectiveName}, here is the playlist").setTimestamp(LocalDateTime.now()).setColor(musicChannel.guild.selfMember.color) + .setFooter("Requested by ${member.effectiveName}", member.user.effectiveAvatarUrl).setDescription( + "**${player.playingTrack.info.title}**, requested by ${if (player.playingTrack.userData is Member) (player.playingTrack.userData as Member).effectiveName else "someone unknown..."} (*now playing*)\n" + + q.joinToString(separator = "\n") { "**${it.info.title}**, requested by ${if (it.userData is Member) (it.userData as Member).effectiveName else "someone unknown..."}" } + ).build() + } + }