Uses 39550K of libraries. Click here for Pure Java version (24143L/127K).
| 1 | !7 | 
| 2 | |
| 3 | /* Fix: | 
| 4 | [JDA [0 / 3] Gateway-Worker 1] WARN WebSocketClient - Hit the WebSocket RateLimit! This can be caused by too many presence or voice status updates (connect/disconnect/mute/deaf). Regular: 3 Voice: 450 Chunking: 0 | 
| 5 | */ | 
| 6 | |
| 7 | static MapSO _renameClasses = litmap( | 
| 8 | "DynDiscordHopper" := "DynShardedDiscordHopper", | 
| 9 | "DynServerAwareDiscordBot" := "DynShardedServerAwareDiscordBot", | 
| 10 | "DynTalkBot2" := "DynShardedTalkBot2"); | 
| 11 | |
| 12 | set flag JDA40. | 
| 13 | |
| 14 | // For now, keep using standard sync collections and JDK 16... | 
| 15 | //set flag OurSyncCollections. | 
| 16 | set flag UseOldLinkedHashSet. | 
| 17 | |
| 18 | // TODO: on restart, check if we actually are in vc with voiceChannelID | 
| 19 | // (if permissions have changed) | 
| 20 | |
| 21 | // Changes in this version: | 
| 22 | // uploadImagesAsFiles | 
| 23 | // !!!listeners fixed | 
| 24 | // check if embed fields are longer than 1k | 
| 25 | |
| 26 | // Note: dm owners only takes ONE LINE | 
| 27 | // TODO: send dms only once when owner has two guilds | 
| 28 | |
| 29 | !include once #1030357 // Discord Audio | 
| 30 | |
| 31 | cmodule MKLabFM extends DynShardedTalkBot2<.ByServer> {
 | 
| 32 | switchable bool doRejoins; | 
| 33 | switchable S specialMsg; | 
| 34 | switchable S radioURL = "http://streamlive2.hearthis.at:8080/26450.ogg"; | 
| 35 | switchable S currentSongURL = "https://widgets.autopo.st/widgets/public/MKLabFM/nowplaying.php"; | 
| 36 | switchable S coverArtURL = "http://95.154.196.33:26687/playingart?sid=1&rand=<RNDINT>"; | 
| 37 | switchable bool enableShadowStop = true; | 
| 38 | switchable bool putSongInStatus = true; | 
| 39 | transient double bufferSeconds = 30.0; | 
| 40 | transient int grabInterval = 15; // try to grab frames quicker to see if it fixes the jumps | 
| 41 | transient double considerDownInterval = 60.0; | 
| 42 | |
| 43 | // restart stream when no frames received for this long | 
| 44 | transient double autoRestartInterval = 20.0; | 
| 45 | |
| 46 | switchable S madeBy = trim([[ | 
| 47 | MKLab: <https://tinyurl.com/musikcolabradiofm> | 
| 48 | Support channel: <https://discord.gg/u6E4muY> | 
| 49 | Bot by: <https://BotCompany.de> | 
| 50 | You like this bot? Donate: <https://paypal.me/BotCompany> | 
| 51 | ]]); | 
| 52 | |
| 53 | switchable S voteLink = "\n\nPlease help us grow by voting here: <https://discordbots.org/bot/625377645982384128>"; | 
| 54 | |
| 55 |   switchable S npMsg = "Now playing: <track> - Listen at }{ https://tinyurl.com/mklab-live-fm }{ #edm #music #radio #ibiza #dance #chillout #psy #live #miami #memes #detroit #techno #dj #synthwave #housemusic #deephouse #onair";
 | 
| 56 | |
| 57 | switchable bool uploadImagesAsFiles = true; | 
| 58 | |
| 59 | transient SimpleCircularBuffer<byte[]> buffer; | 
| 60 | transient AudioPlayer player; | 
| 61 | switchable transient bool simulateStreamDown, restartNow; | 
| 62 | transient long lastFrameReceived, lastActualFrameReceived; // sys time | 
| 63 | transient long moduleStarted; // sys time | 
| 64 | transient NotTooOften postRestarting = onlyEveryHour(); | 
| 65 | transient S currentSong; | 
| 66 | |
| 67 | // stats | 
| 68 | transient new AtomicLong framesReceived; | 
| 69 | transient new AtomicLong canProvideCalls; | 
| 70 | transient new AtomicLong provideCalls; | 
| 71 | transient new AtomicLong dataSent; | 
| 72 | |
| 73 |   O _getReloadData() { ret currentSong; }
 | 
| 74 |   void _setReloadData(S data) { currentSong = data; }
 | 
| 75 | |
| 76 |   void init {
 | 
| 77 | super.init(); | 
| 78 | myName = "!!!"; | 
| 79 | makeByServer = () -> new ByServer; | 
| 80 | dropPunctuation = false; | 
| 81 | preprocessAtSelfToMyName = true; | 
| 82 | ngbCommands = false; | 
| 83 | sendToPostedLinesCRUD = false; | 
| 84 | |
| 85 | // First thing, start the stream, so we can restart the radios | 
| 86 | initStream(); | 
| 87 | |
| 88 |     onDiscordStarted(r {
 | 
| 89 | temp enter(); | 
| 90 |       print("Bot back up, slowly rejoining voice channels");
 | 
| 91 | |
| 92 | if (doRejoins) | 
| 93 |         thread "Rejoin voice channels" {
 | 
| 94 | var map = cloneMap(dataByServer); | 
| 95 | int n = 0; | 
| 96 |           for (Long id, ByServer bs : map) {
 | 
| 97 | if (!licensed()) ret; | 
| 98 |             pcall {
 | 
| 99 | bs.restart(discord.getGuildById(id)); | 
| 100 | } | 
| 101 | sleep(5); | 
| 102 | if ((++n % 100) == 0) | 
| 103 |               print("Processed " + n2(n) + " of " + n2(map) + " servers");
 | 
| 104 | } | 
| 105 |           print("Processed all servers");
 | 
| 106 | |
| 107 | postInChannel(nextGenBotsTestRoomID(), "Rebooted " + computerID() + "/" + dm_moduleID()); | 
| 108 | } | 
| 109 | |
| 110 | }); | 
| 111 | |
| 112 |     onDiscordEvent(e -> {
 | 
| 113 | if (e cast GenericGuildVoiceEvent) | 
| 114 | getByServer(e.getGuild()).handleVoiceUpdateEvent(e); | 
| 115 | }); | 
| 116 | |
| 117 | doEveryAndNow(20.0, r grabSong); | 
| 118 | doEvery(60.0, r updateVoiceChannelCount_allServers); | 
| 119 | } | 
| 120 | |
| 121 |   void initStream {
 | 
| 122 | buffer = new SimpleCircularBuffer(iround(bufferSeconds*50)); | 
| 123 | player = lavaPlayer_createPlayer(); | 
| 124 | lavaPlayer_printPlayerEvents(player); | 
| 125 |     print("Have player");
 | 
| 126 | |
| 127 |     doEvery(grabInterval, r {
 | 
| 128 | temp enter(); | 
| 129 |       if (!simulateStreamDown) {
 | 
| 130 | AudioFrame frame = player.provide(); | 
| 131 |         if (frame != null) {
 | 
| 132 | buffer.add(frame.getData()); | 
| 133 | inc(framesReceived); | 
| 134 | lastFrameReceived = lastActualFrameReceived = sysNow(); | 
| 135 | } | 
| 136 | } | 
| 137 | }); | 
| 138 | |
| 139 |     doEvery(1.0, r {
 | 
| 140 | temp enter(); | 
| 141 | if (discord == null) ret; | 
| 142 |       if (restartNow || elapsedSeconds(lastFrameReceived) >= autoRestartInterval) {
 | 
| 143 | if (postRestarting.yo()) | 
| 144 | postInChannel(nextGenBotsTestRoomID(), "Restarting radio stream"); | 
| 145 | simulateStreamDown = restartNow = false; | 
| 146 | lastFrameReceived = sysNow(); | 
| 147 | loadTrack(); | 
| 148 | lastFrameReceived = sysNow(); | 
| 149 | } | 
| 150 | }); | 
| 151 | } | 
| 152 | |
| 153 |   class ByServer extends DynShardedTalkBot2.ByServer {
 | 
| 154 | bool shouldBePlaying; | 
| 155 | long voiceChannelID, lastVoiceChannelID; | 
| 156 | long songChannelID; | 
| 157 | |
| 158 | transient S voiceChannelName; | 
| 159 | transient VoiceChannel vc; | 
| 160 | transient MyAudioSendHandler sendHandler; | 
| 161 | transient Set<Long> usersInVoiceChannel = synchroSet(); | 
| 162 | transient new AtomicLong canProvideCallsInChannel; | 
| 163 | transient new AtomicLong provideCallsInChannel; | 
| 164 | transient new AtomicLong dataSentInChannel; | 
| 165 | transient byte lastAudioByteSent; | 
| 166 | |
| 167 |     synchronized S processSimplifiedLine(S s, O... _) enter {
 | 
| 168 |       try {
 | 
| 169 | ret processSimplifiedLine2(s, _); | 
| 170 |       } catch print e {
 | 
| 171 | if (cic(str(e), "InsufficientPermission")) | 
| 172 | ret "There were insufficient permissions to perform this action."; | 
| 173 | ret "Sorry, an error occurred trying to perform this action."; | 
| 174 | } | 
| 175 | } | 
| 176 | |
| 177 |     synchronized S processSimplifiedLine2(S s, O... _) {
 | 
| 178 | try answer super.processSimplifiedLine(s, _); // add authorized users etc. | 
| 179 | new Matches m; | 
| 180 | |
| 181 |       //print("myPrefix", quote(myPrefix()));
 | 
| 182 | s = dropMyPrefixOrNull(s); | 
| 183 |       //print("dropped", quote(s));
 | 
| 184 | if (s == null) null; | 
| 185 | optPar Message msg; | 
| 186 | Guild guild = msg.getGuild(); | 
| 187 | Member member = msg == null ? null : msg.getMember(); | 
| 188 | |
| 189 |       if (eqic(s, "help")) {
 | 
| 190 | optPar MessageChannel channel; | 
| 191 | |
| 192 | S syntax = trim([[ | 
| 193 | `@me play | start` - start playing | 
| 194 | `@me stop | pause` - stop playing | 
| 195 | `@me leave` - leave voice channel | 
| 196 | `@me help` - show this help | 
| 197 | `@me np` - show currently playing song | 
| 198 | `@me join` - join any voice channel | 
| 199 | `@me set channel` - post now playing track names in current channel | 
| 200 | `@me stop posting` - stop posting track names | 
| 201 | [MORE] | 
| 202 | ]].replace("\n[MORE]", renderChannelListIfNotOne(guild))
 | 
| 203 |   .replace("@me ", myPrefix()))
 | 
| 204 | + voteLink; | 
| 205 | |
| 206 | new EmbedBuilder eb; | 
| 207 | eb.setColor(Color.yellow); | 
| 208 |         eb.setAuthor("MKLab FM Bot", null, null);
 | 
| 209 |         eb.setTitle("Purpose", null);
 | 
| 210 |         eb.setDescription("I play MKLab FM, the best Electronic Dance Music Radio, in a voice channel of your choice.");
 | 
| 211 | discordEmbed_addField(eb, "Syntax", syntax); | 
| 212 | |
| 213 | if (authed(_)) | 
| 214 |           eb.addField("Admin commands", rtrim([[
 | 
| 215 | `@me guild count/guild names` - show stats | 
| 216 | `@me reboot` - reboot the whole bot (if music stream is completely broken for some reason) | 
| 217 | `@me dm owners: <message>` - DM all guild owners where radio is playing | 
| 218 | `@me stream url http...` - set stream URL. current: $STREAMURL | 
| 219 | `@me np url http...` - set current song URL. current: $NPURL | 
| 220 | `@me cover url http...` - set cover art URL. current: $COVERURL | 
| 221 | `@me cover url -` - disable cover art | 
| 222 | `@me np msg Playing <track>...` - set nowplaying message. current: $NPMSG | 
| 223 | `@me listeners` - full list of listeners | 
| 224 | ]]) | 
| 225 |   .replace("@me ", myPrefix())
 | 
| 226 |   .replace("$STREAMURL", backtickQuote(radioURL))
 | 
| 227 |   .replace("$NPURL", backtickQuote(currentSongURL))
 | 
| 228 |   .replace("$NPMSG", backtickQuote(npMsg))
 | 
| 229 |   .replace("$COVERURL", backtickQuote(coverArtURL)), false);
 | 
| 230 | |
| 231 |         eb.addField("Made by", madeBy, false);
 | 
| 232 | |
| 233 | eb.setThumbnail(imageSnippetURL(#1102722)); | 
| 234 | channel.sendMessage(eb.build()).queue(); | 
| 235 | null; | 
| 236 | } | 
| 237 | |
| 238 | if (guild == null) null; | 
| 239 | |
| 240 |       if (authed(_)) {
 | 
| 241 | /*if "restart radios" | 
| 242 | ret "OK" with restartRadios();*/ | 
| 243 |         if "reboot" {
 | 
| 244 | doAfter(2.0, rEnter dm_reload); | 
| 245 | ret "OK, rebooting bot"; | 
| 246 | } | 
| 247 |         if "simulate stream down" {
 | 
| 248 | simulateStreamDown = true; | 
| 249 | ret "OK"; | 
| 250 | } | 
| 251 | if (swic_trim(s, "dm owners:", m)) | 
| 252 | ret dmOwners($1); | 
| 253 | |
| 254 |         if (swic_trim(s, "stream url ", m) && isWebURL(m.rest())) {
 | 
| 255 | if (!MKLabFM.this.setField(radioURL := m.rest())) ret "No change"; | 
| 256 | restartNow = true; | 
| 257 | ret "OK, restarting with radio URL " + radioURL; | 
| 258 | } | 
| 259 | |
| 260 |         if (swic_trim(s, "np url ", m) && isWebURL(m.rest())) {
 | 
| 261 | if (!MKLabFM.this.setField(currentSongURL := m.rest())) ret "No change"; | 
| 262 | ret "Current song URL changed to " + currentSongURL; | 
| 263 | } | 
| 264 | |
| 265 |         if (swic_trim(s, "cover url ", m)) {
 | 
| 266 | if (!MKLabFM.this.setField(coverArtURL := m.rest())) ret "No change"; | 
| 267 | ret "Cover art URL changed to " + coverArtURL; | 
| 268 | } | 
| 269 | |
| 270 |         if (swic_trim(s, "np msg ", m)) {
 | 
| 271 | if (!MKLabFM.this.setField(npMsg := m.rest())) ret "No change"; | 
| 272 | ret "Nowplaying message changed to " + npMsg; | 
| 273 | } | 
| 274 | |
| 275 | if "listeners" | 
| 276 | ret or2(listeningUsers_full(), "No listeners"); | 
| 277 | } | 
| 278 | |
| 279 |       if (matchX2("play ...|start...|radio|radio on", s))
 | 
| 280 | ret action_play(guild, member); | 
| 281 | |
| 282 |       if (matchX2("stop ...|pause ...|quiet|silence|shut up|... off", s) && !match("stop posting", s)) 
 | 
| 283 | ret action_stop(guild); | 
| 284 | |
| 285 |       if (matchX2("leave|leave channel|leave voice channel", s))
 | 
| 286 | ret action_leave(guild); | 
| 287 | |
| 288 |       if (matchX2("join|join channel|join voice channel", s))
 | 
| 289 | ret action_join(guild, member); | 
| 290 | |
| 291 |       if (matchX2("... channel *", s, m) && isInteger($1))
 | 
| 292 | ret action_selectChannel(guild, parseInt($1)); | 
| 293 | |
| 294 |       if (matchX2("np|n p|... song ...|... playing ...", s))
 | 
| 295 | ret "MKLab FM is currently playing: " + (nempty(currentSong) ? backtickQuote(currentSong) : "Hmm. Not sure, I think something went wrong."); | 
| 296 | |
| 297 |       if (matchX2("set channel|post here|set np", s)) {
 | 
| 298 | long channelID = longPar channelID(_); | 
| 299 | if (channelID == 0) ret "Please do this in a channel"; | 
| 300 |         if (songChannelID != channelID) {
 | 
| 301 | songChannelID = channelID; | 
| 302 | change(); | 
| 303 | } | 
| 304 | doAfter(2.0, r postSong); | 
| 305 | ret "OK, I will post the currently playing tracks in this channel. Type `" + atSelfOrMyName_space() + "stop posting` to stop."; | 
| 306 | } | 
| 307 | |
| 308 |       if (matchX2("stop posting|no channel", s)) {
 | 
| 309 | songChannelID = 0; | 
| 310 | change(); | 
| 311 | ret "OK"; | 
| 312 | } | 
| 313 | |
| 314 | if "users in channel" | 
| 315 | ret "Users in voice channel: " + n2(usersInVoiceChannel); | 
| 316 | |
| 317 | if "internal stats" | 
| 318 | ret "All servers - frames received: " + n2(framesReceived!) | 
| 319 | + ", canProvide calls: " + n2(canProvideCalls!) | 
| 320 | + ", provide calls: " + n2(provideCalls!) | 
| 321 | + ", bytes sent: " + n2(dataSent!) | 
| 322 | + ", buffer fill grade: " + bufferStats() + "\n" | 
| 323 | + "This server - should be playing: " + shouldBePlaying | 
| 324 | + ", shadow stopped: " + shadowStopped() | 
| 325 | + ", users in voice channel: " + l(usersInVoiceChannel) | 
| 326 | + ", canProvide calls: " + n2(canProvideCallsInChannel!) | 
| 327 | + ", provide calls: " + n2(provideCallsInChannel!) | 
| 328 | + ", data: " + l(sendHandler?.data) | 
| 329 | + ", data sent: " + n2(dataSentInChannel!) | 
| 330 | + ", last audio byte: " + lastAudioByteSent; | 
| 331 | |
| 332 | null; | 
| 333 | } | 
| 334 | |
| 335 |     S bufferStats() {
 | 
| 336 |       synchronized(buffer) {
 | 
| 337 | ret buffer.size() + "/" + buffer.capacity(); | 
| 338 | } | 
| 339 | } | 
| 340 | |
| 341 |     S renderGuildCount() {
 | 
| 342 | L<Guild> guilds = discord.getGuilds(); | 
| 343 | int shouldPlayCount = 0, shadowCount = 0; | 
| 344 |       for (Guild guild : guilds) {
 | 
| 345 | ByServer bs = getByServer(guild.getIdLong(), true); | 
| 346 |         if (bs != null) {
 | 
| 347 | //if (bs.playing) ++playCount; | 
| 348 |           if (bs.shouldBePlaying) {
 | 
| 349 | ++shouldPlayCount; | 
| 350 | if (bs.shadowStopped()) ++shadowCount; | 
| 351 | } | 
| 352 | } | 
| 353 | } | 
| 354 | ret super.renderGuildCount() + ". " + nRadios(shouldPlayCount-shadowCount) + " playing to " + n2(listeningUsers(), "listener") + ", " + shadowCount + " playing without listeners"; | 
| 355 | } | 
| 356 | |
| 357 |     bool joinVoiceChannel(Guild guild) {
 | 
| 358 | vc = guild.getVoiceChannelById(voiceChannelID); | 
| 359 | if (vc == null) false; | 
| 360 | guild.getAudioManager().openAudioConnection(vc); | 
| 361 | voiceChannelName = vc.getName(); | 
| 362 | updateVoiceChannelCount(); | 
| 363 | true; | 
| 364 | } | 
| 365 | |
| 366 |     void updateVoiceChannelCount {
 | 
| 367 | if (vc == null) ret with usersInVoiceChannel.clear(); | 
| 368 | L<Member> members = vc.getMembers(); | 
| 369 | int n = l(usersInVoiceChannel); | 
| 370 | replaceCollection(usersInVoiceChannel, | 
| 371 |         mapNonNulls_pcall(members, m -> {
 | 
| 372 | User user = m.getUser(); | 
| 373 | ret user.isBot() ? null : user.getIdLong(); | 
| 374 | })); | 
| 375 | int n2 = l(usersInVoiceChannel); | 
| 376 | if (n != n2) print(guildID + " voice channel count corrected: " + n + " -> " + n2); | 
| 377 | } | 
| 378 | |
| 379 |     bool shadowStopped() {
 | 
| 380 | ret enableShadowStop && empty(usersInVoiceChannel); | 
| 381 | } | 
| 382 | |
| 383 |     class MyAudioSendHandler implements AudioSendHandler {
 | 
| 384 | long counter = -1; | 
| 385 | byte[] data; | 
| 386 | |
| 387 |       public bool canProvide() enter {
 | 
| 388 | inc(canProvideCalls); | 
| 389 | inc(canProvideCallsInChannel); | 
| 390 | |
| 391 | if (!shouldBePlaying) false; | 
| 392 |         if (shadowStopped()) {
 | 
| 393 | counter = -1; | 
| 394 | false; | 
| 395 | } | 
| 396 | |
| 397 |         synchronized(buffer) {
 | 
| 398 | if (!buffer.isFull()) false; | 
| 399 | |
| 400 | // out of range? go back to middle | 
| 401 | data = buffer.get(counter); | 
| 402 |           if (data == null) {
 | 
| 403 | long oldCounter = counter; | 
| 404 | counter = buffer.getBase()+buffer.size()/2; | 
| 405 |             if (oldCounter >= 0) printWithTime("RESETTING " + oldCounter + " => " + counter
 | 
| 406 | + " (delta=" + (counter-oldCounter) + ")"); | 
| 407 | data = buffer.get(counter); | 
| 408 | } | 
| 409 | ++counter; | 
| 410 | true; | 
| 411 | } | 
| 412 | } | 
| 413 | |
| 414 |       public java.nio.ByteBuffer provide20MsAudio() {
 | 
| 415 | inc(provideCalls); | 
| 416 | inc(provideCallsInChannel); | 
| 417 | inc(dataSent, l(data)); | 
| 418 | inc(dataSentInChannel, l(data)); | 
| 419 | lastAudioByteSent = last(data); | 
| 420 | java.nio.ByteBuffer buf = java.nio.ByteBuffer.wrap(data); | 
| 421 | ret buf; | 
| 422 | } | 
| 423 | |
| 424 |       public bool isOpus() { true; }
 | 
| 425 | } | 
| 426 | |
| 427 | // when already in channel | 
| 428 |     S startPlaying(Guild guild) {
 | 
| 429 | AudioManager am = guild.getAudioManager(); | 
| 430 | sendHandler = new MyAudioSendHandler; | 
| 431 | am.setSendingHandler(sendHandler); | 
| 432 | |
| 433 | ret "Playing radio in channel " + quote(voiceChannelName); | 
| 434 | } | 
| 435 | |
| 436 | // try to select the best channel | 
| 437 | // -the one the user is in, or | 
| 438 | // -the one that was used before, or | 
| 439 | // -just any channel. | 
| 440 | // returns error message or null | 
| 441 |     S selectAVoiceChannel(Guild guild, Member member) {
 | 
| 442 |       print("selectAVoiceChannel member=" + member);
 | 
| 443 |       if (member != null) {
 | 
| 444 | GuildVoiceState vs = member.getVoiceState(); | 
| 445 | VoiceChannel vc = vs.getChannel(); | 
| 446 |         print("selectAVoiceChannel vc=" + vc);
 | 
| 447 |         if (vc != null) {
 | 
| 448 | voiceChannelID = lastVoiceChannelID = vc.getIdLong(); | 
| 449 | ret null with change(); | 
| 450 | } | 
| 451 | } | 
| 452 | |
| 453 | if (voiceChannelID != 0) null; | 
| 454 | L<VoiceChannel> allChannels = guild.getVoiceChannels(); | 
| 455 | L<VoiceChannel> channels = filter(allChannels, c -> isJoinableVoiceChannel(guild, c)); | 
| 456 | |
| 457 | if (empty(channels)) | 
| 458 | if (empty(allChannels)) | 
| 459 | ret "Please create a voice channel."; | 
| 460 | else | 
| 461 | ret "Sorry, I am not allowed to enter any of your voice channels. Maybe my invite link was wrong?"; // TODO: show better link | 
| 462 | |
| 463 | // Check if lastVoiceChannelID still exists | 
| 464 |       if (lastVoiceChannelID != 0 && !hasWhereMethodReturns getIdLong(channels, lastVoiceChannelID)) {
 | 
| 465 | lastVoiceChannelID = 0; change(); | 
| 466 | } | 
| 467 | |
| 468 | voiceChannelID = lastVoiceChannelID != 0 ? lastVoiceChannelID | 
| 469 | : first(channels).getIdLong(); | 
| 470 | change(); | 
| 471 | null; | 
| 472 | } | 
| 473 | |
| 474 |     S action_play(Guild guild, Member member) {
 | 
| 475 | if (shouldBePlaying && sendHandler != null) ret "I am playing already!"; | 
| 476 | try answer selectAVoiceChannel(guild, member); | 
| 477 | joinVoiceChannel(guild); | 
| 478 | shouldBePlaying = true; change(); | 
| 479 | ret startPlaying(guild); | 
| 480 | } | 
| 481 | |
| 482 |     S action_stop(Guild guild) {
 | 
| 483 |       if (shouldBePlaying) {
 | 
| 484 | shouldBePlaying = false; change(); | 
| 485 | ret "OK, radio switched off"; | 
| 486 | } | 
| 487 | ret "I am stopped, I think. No?"; | 
| 488 | } | 
| 489 | |
| 490 |     S action_leave(Guild guild) {
 | 
| 491 | voiceChannelID = 0; change(); | 
| 492 | this.vc = null; | 
| 493 | GuildVoiceState state = getSelfMember(guild).getVoiceState(); | 
| 494 | VoiceChannel vc = state.getChannel(); | 
| 495 | if (vc == null) ret "I don't think I am in any voice channel"; | 
| 496 | guild.getAudioManager().closeAudioConnection(); | 
| 497 | ret "OK, leaving channel " + quote(vc.getName()); | 
| 498 | } | 
| 499 | |
| 500 |     S action_join(Guild guild, Member member) {
 | 
| 501 | GuildVoiceState state = getSelfMember(guild).getVoiceState(); | 
| 502 | VoiceChannel vc = state.getChannel(); | 
| 503 | try answer selectAVoiceChannel(guild, member); | 
| 504 | joinVoiceChannel(guild); | 
| 505 | ret "OK, joined voice channel " + quote(voiceChannelName); | 
| 506 | } | 
| 507 | |
| 508 |     S action_selectChannel(Guild guild, int i) {
 | 
| 509 | L<VoiceChannel> channels = guild.getVoiceChannels(); | 
| 510 | channels = filter(channels, c -> isJoinableVoiceChannel(guild, c)); | 
| 511 | VoiceChannel vc = _get(channels, i-1); | 
| 512 | if (vc == null) ret "Sorry, I count only " + nChannels(channels); | 
| 513 | voiceChannelID = lastVoiceChannelID = vc.getIdLong(); | 
| 514 | change(); | 
| 515 | joinVoiceChannel(guild); | 
| 516 | ret "OK, joined voice channel " + quote(voiceChannelName); | 
| 517 | } | 
| 518 | |
| 519 |     S renderChannelListIfNotOne(Guild guild) {
 | 
| 520 | if (guild == null) ret ""; | 
| 521 | L<VoiceChannel> channels = guild.getVoiceChannels(); | 
| 522 | channels = filter(channels, c -> isJoinableVoiceChannel(guild, c)); | 
| 523 | if (empty(channels)) ret "\nNote: You don't have any voice channels."; | 
| 524 | if (l(channels) == 1) ret ""; | 
| 525 | new LS out; | 
| 526 | for i over channels: | 
| 527 |         out.add("`@me join channel " + (i+1) + "` - join channel " + quote(channels.get(i).getName()));
 | 
| 528 | ret "\n" + lines_rtrim(out); | 
| 529 | } | 
| 530 | |
| 531 |     bool isJoinableVoiceChannel(Guild guild, VoiceChannel vc) {
 | 
| 532 | if (vc == null || guild == null) false; | 
| 533 |       try {
 | 
| 534 | ret guild.getSelfMember().hasPermission(vc, Permission.VOICE_CONNECT); | 
| 535 |       } catch print e { ret false; }
 | 
| 536 | } | 
| 537 | |
| 538 | // normal restart (on bot load) | 
| 539 |     void restart(Guild guild) {
 | 
| 540 | if (guild == null) ret; // no longer in guild | 
| 541 |       if (shouldBePlaying) {
 | 
| 542 |         print("RESTARTING RADIO in " + guild);
 | 
| 543 | print(action_play(guild, null)); | 
| 544 |       } else if (voiceChannelID != 0) {
 | 
| 545 |         print("Rejoining voice channel in " + guild);
 | 
| 546 | print(action_join(guild, null)); | 
| 547 | } | 
| 548 | } | 
| 549 | |
| 550 | // restart with stop (after stream interruption) - no longer needed | 
| 551 |     /*void hardRestart(Guild guild) {
 | 
| 552 | restart(guild); | 
| 553 | }*/ | 
| 554 | |
| 555 |     void postSong {     
 | 
| 556 | if (songChannelID == 0) ret; | 
| 557 |       postInChannel(songChannelID, npMsg.replace("<track>", backtickQuote(currentSong)));
 | 
| 558 |       if (isWebURL(coverArtURL)) {
 | 
| 559 | S imageURL = appendQueryToURL(coverArtURL, rand := randomInt()); | 
| 560 | if (uploadImagesAsFiles) | 
| 561 | uploadFileInChannel(songChannelID, loadBinaryPage(imageURL), "cover-art.jpg", "", null); | 
| 562 | else | 
| 563 | postImage(getChannel(songChannelID), imageURL, ""); | 
| 564 | } | 
| 565 | } | 
| 566 | |
| 567 |     void handleUserJoined(long userID, VoiceChannel joined) {
 | 
| 568 |       if (joined != null && joined.getIdLong() == voiceChannelID) {
 | 
| 569 | usersInVoiceChannel.add(userID); | 
| 570 | print(guildID + " adding user to voice channel: " + userID + " (n=" + l(usersInVoiceChannel)); | 
| 571 | } | 
| 572 | } | 
| 573 | |
| 574 |     void handleVoiceUpdateEvent(GenericGuildVoiceEvent e) {
 | 
| 575 | User user = ((GenericGuildVoiceEvent) e).getMember().getUser(); | 
| 576 | if (user.isBot()) ret; | 
| 577 | long userID = user.getIdLong(); | 
| 578 |       if (e cast GuildVoiceUpdateEvent) {
 | 
| 579 | VoiceChannel left = e.getChannelLeft(); | 
| 580 |         if (e.getChannelLeft().getIdLong() == voiceChannelID) {
 | 
| 581 | usersInVoiceChannel.remove(userID); | 
| 582 | print(guildID + " removed user from voice channel: " + userID + " (n=" + l(usersInVoiceChannel)); | 
| 583 | } | 
| 584 | } | 
| 585 | if (e cast GuildVoiceMoveEvent) | 
| 586 | handleUserJoined(userID, e.getChannelJoined()); | 
| 587 | if (e cast GuildVoiceJoinEvent) | 
| 588 | handleUserJoined(userID, e.getChannelJoined()); | 
| 589 | } | 
| 590 | } // end of ByServer | 
| 591 | |
| 592 |   /*void restartRadios {
 | 
| 593 | L<Guild> guilds = discord.getGuilds(); | 
| 594 |     for (Guild guild : guilds) pcall {
 | 
| 595 | ByServer bs = getByServer(guild); | 
| 596 | if (bs != null) | 
| 597 | bs.hardRestart(guild); | 
| 598 | } | 
| 599 | }*/ | 
| 600 | |
| 601 |   S dmOwners(S msg) {
 | 
| 602 | L<Guild> guilds = discord.getGuilds(); | 
| 603 | int n = 0; | 
| 604 |     for (Guild guild : guilds) pcall {
 | 
| 605 | ByServer bs = getByServer(guild.getIdLong(), true); | 
| 606 |       if (bs != null && bs.shouldBePlaying) {
 | 
| 607 | gazelle_dmGuildOwner(guild, msg); | 
| 608 | ++n; | 
| 609 | } | 
| 610 | } | 
| 611 | ret n2(n, "guild owner") + " contacted."; | 
| 612 | } | 
| 613 | |
| 614 |   void loadTrack {
 | 
| 615 | if (!isURL(radioURL)) ret; | 
| 616 |     lavaPlayer_loadItem(module(), lavaPlayer_playerManager(), player, radioURL.replace("<RNDINT>", str(randomInt())));
 | 
| 617 | } | 
| 618 | |
| 619 |   S currentSong() {
 | 
| 620 | temp tempSetTL(loadPage_sizeLimit, 128*1024L); | 
| 621 | if (l(currentSongURL) <= 1) ret ""; | 
| 622 | if (cic(currentSongURL, "autopo.st/")) | 
| 623 | ret trackTitleFromAutoPostWidget(loadPageWithTimeout(currentSongURL, 10.0)); | 
| 624 | if (cic(currentSongURL, "caster.fm/")) | 
| 625 | ret (S) mapGet(jsonDecodeMap(loadPageWithHeaders(currentSongURL, | 
| 626 | "X-Requested-With", "XMLHttpRequest")), "playing"); | 
| 627 | if (endsWith(currentSongURL, "status.xsl")) | 
| 628 | ret trackTitleFromIceCastStatusXSL(loadPageWithTimeout(currentSongURL, 10.0)); | 
| 629 | ret loadPageWithTimeout(currentSongURL, 10.0); | 
| 630 | } | 
| 631 | |
| 632 |   void grabSong enter {
 | 
| 633 | if (discord == null) ret; | 
| 634 | S song = currentSong(); | 
| 635 | bool post = nempty(currentSong) && nempty(song) && neq(currentSong, song); | 
| 636 | currentSong = song; | 
| 637 | if (post) | 
| 638 | postSong_allServers(); | 
| 639 |     if (putSongInStatus) {
 | 
| 640 | S s = currentSong; | 
| 641 | if (nempty(specialMsg)) | 
| 642 | s = specialMsg; | 
| 643 | else if (elapsedSeconds(lastActualFrameReceived) >= considerDownInterval && elapsedSeconds(moduleStarted) >= considerDownInterval) | 
| 644 | s = "Stream currently down"; | 
| 645 |       if (nempty(s)) {
 | 
| 646 | Activity activity = Activity.playing(s); | 
| 647 | discord.setPresence(OnlineStatus.ONLINE, activity); | 
| 648 | } | 
| 649 | } | 
| 650 | } | 
| 651 | |
| 652 |   void postSong_allServers {
 | 
| 653 | L<Guild> guilds = discord.getGuilds(); | 
| 654 |     for (Guild guild : guilds) pcall {
 | 
| 655 | ByServer bs = getByServer(guild); | 
| 656 | if (bs != null) | 
| 657 | bs.postSong(); | 
| 658 | } | 
| 659 | } | 
| 660 | |
| 661 |   void updateVoiceChannelCount_allServers {
 | 
| 662 | if (discord == null) ret; | 
| 663 | for (ByServer bs : cloneValues(dataByServer)) | 
| 664 | bs.updateVoiceChannelCount(); | 
| 665 | } | 
| 666 | |
| 667 |   int listeningUsers() {
 | 
| 668 | int n = 0; | 
| 669 |     for (Guild guild : discord.getGuilds()) {
 | 
| 670 | ByServer bs = getByServer(guild.getIdLong(), true); | 
| 671 | if (bs != null && bs.shouldBePlaying) | 
| 672 | n += l(bs.usersInVoiceChannel); | 
| 673 | } | 
| 674 | ret n; | 
| 675 | } | 
| 676 | |
| 677 |   S listeningUsers_full() {
 | 
| 678 | new LS out; | 
| 679 |     for (Guild guild : discord.getGuilds()) {
 | 
| 680 | ByServer bs = getByServer(guild.getIdLong(), true); | 
| 681 |       if (bs != null && bs.shouldBePlaying) {
 | 
| 682 | L<Long> users = cloneList(bs.usersInVoiceChannel); | 
| 683 | if (empty(users)) continue; | 
| 684 | out.add(guild.getName() + ": playing to " + joinWithComma(map(users, | 
| 685 | id -> or2(discord_memberIDToEffectiveName(guild, id), str(id))))); | 
| 686 | } | 
| 687 | } | 
| 688 | ret lines(out); | 
| 689 | } | 
| 690 | } | 
Began life as a copy of #1025354
download show line numbers debug dex old transpilations
Travelled to 4 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, vouqrxazstgt
No comments. add comment
| Snippet ID: | #1030356 | 
| Snippet name: | MKLab FM Bot with JDA 4.2 [LIVE, fixing status & playback for shards] | 
| Eternal ID of this version: | #1030356/44 | 
| Text MD5: | 735cb7cecb1ec2115fa56ba4168807f2 | 
| Transpilation MD5: | e750597f5761de1568c76d92bcfe3f03 | 
| Author: | stefan | 
| Category: | javax | 
| Type: | JavaX source code (Dynamic Module) | 
| Public (visible to everyone): | Yes | 
| Archived (hidden from active list): | No | 
| Created/modified: | 2022-05-05 14:44:52 | 
| Source code size: | 24942 bytes / 690 lines | 
| Pitched / IR pitched: | No / No | 
| Views / Downloads: | 496 / 1284 | 
| Version history: | 43 change(s) | 
| Referenced in: | [show references] |