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: | 261 / 993 |
Version history: | 43 change(s) |
Referenced in: | [show references] |