import net.dv8tion.jda.core.events.guild.member.*; import net.dv8tion.jda.core.Permission; import net.dv8tion.jda.core.requests.restaction.MessageAction; import net.dv8tion.jda.core.requests.RestAction; import java.util.function.Consumer; switchable S discordToken; S discordBotName; long discordBotID; bool reactToBots = true; transient bool sendToPostedLinesCRUD = true; transient JDA discord; transient Color discordImageEmbedMysteriousLineColor = colorFromHex("36393f"); transient L> onPostedInChannel = syncList(); //transient bool debugPosting; // when a discord action failed transient Consumer onQueueError = error -> { temp enter(); printStackTrace(error); }; void startDiscord { if (!discordEnabled()) ret with print("Not enabled"); vm_cleanPrints(); // avoid Swing Unicode problem logModuleOutput(); // good idea for chat bots // TODO: log JDA output going to System.out/err discord = discordBot(new ListenerAdapter { @Override public void onMessageUpdate(MessageUpdateEvent e) pcall { temp enter(); ret if !discordEnabled() || !licensed(); Message msg = e.getMessage(); long msgID = msg.getIdLong(); User user = e.getAuthor(); long userID = user == null ? 0 : user.getIdLong(); S content = e.getMessage().getContentRaw(); MessageChannel channel = e.getChannel(); long channelID = channel.getIdLong(); S rendered = msgID + ": " + content; //print("Message edited: " + rendered); vmBus_send editedDiscordMessage( litmapparams(event := e, module := dm_me(), +msgID, +msg, +content, +channelID, +channel, +user, +userID)); O lineConcept = dm_discord_lineForMsgID_unimported(msgID); if (lineConcept == null) ret; // logging module not enabled call(lineConcept, '_setField, editedText := content); } @Override public void onMessageReceived(MessageReceivedEvent e) pcall { temp enter(); ret if !discordEnabled() || !licensed(); // send out raw event vmBus_send discord_onMessageReceived(module(), e); User user = e.getAuthor(); if (!reactToUser(user)) ret with print("Not reacting to user " + user); bool bot = user.isBot(); long msgID = e.getMessage().getIdLong(); long userID = user.getIdLong(); Guild guild = e.getGuild(); long guildID = toLong(call(guild, 'getIdLong); Member member = e.getMember(); S userName = member == null ? null : member.getNickname(); // the changeable nick name - null sometimes? if (userName == null && member != null) userName = member.getEffectiveName(); if (userName == null) userName = e.getAuthor().getName(); final Message msg = e.getMessage(); MessageChannel channel = e.getChannel(); long channelID = channel.getIdLong(); //print("Channel type: " + e.getChannelType()); bool isPrivate = e.getChannelType() == ChannelType.PRIVATE; S content = trim(msg.getContentRaw()); print("Msg from " + userName + ": " + content); vmBus_send incomingDiscordMessage( litmapparams(fromBot := bot, module := dm_me(), +msgID, +userID, +userName, event := e, +guildID, +guild, +member, +msg, +content, +isPrivate, +channelID, +channel)); } @Override public void onMessageReactionAdd(MessageReactionAddEvent e) pcall { temp enter(); ret if !discordEnabled() || !licensed(); MessageReaction r = e.getReaction(); User user = e.getUser(); if (!reactToUser(user)) ret; bool bot = user.isBot(); long msgID = r.getMessageIdLong(); S emoji = r.getReactionEmote().getName(); vmBus_send('incomingDiscordReaction, litmapparams( reaction := r, fromBot := bot, +user, userID := user.getIdLong(), module := dm_me(), +msgID, +emoji )); } @Override public void onMessageReactionRemove(MessageReactionRemoveEvent e) pcall { temp enter(); ret if !discordEnabled() || !licensed(); MessageReaction r = e.getReaction(); User user = e.getUser(); if (!reactToUser(user)) ret; bool bot = user.isBot(); long msgID = r.getMessageIdLong(); S emoji = r.getReactionEmote().getName(); vmBus_send('discordReactionRemoved, litmapparams( reaction := r, fromBot := bot, +user, userID := user.getIdLong(), module := dm_me(), +msgID, +emoji )); } @Override public void onMessageReactionRemoveAll(MessageReactionRemoveAllEvent e) pcall { temp enter(); print("TODO: onMessageReactionRemoveAll"); } @Override public void onGuildMemberJoin(GuildMemberJoinEvent event) pcall { temp enter(); print("Got guild join"); ret if !discordEnabled() || !licensed(); print("Join >> getting user ID"); long userID = rcall_long getIdLong(rcall getUser(event.getMember())); print("Join >> sending"); vmBus_send('discordGuildJoin, litmapparams( module := dm_me(), +event, +userID, guildID := discord_guildIDFromEvent_gen(event) )); } @Override public void onGuildMemberLeave(GuildMemberLeaveEvent event) pcall { temp enter(); print("Got guild leave"); ret if !discordEnabled() || !licensed(); long userID = rcall_long getIdLong(rcall getUser(event.getMember())); vmBus_send('discordGuildLeave, litmapparams( module := dm_me(), +event, +userID )); } }, token := discordToken); dm_registerAs('liveDiscordModule); dm_vmBus_answerToMessage activeDiscordTokens(func -> LS { ll(discordToken) }); pcall { setField(discordBotName := jda_selfUserName(discord)); setField(discordBotID := jda_selfUserID(discord)); } print("Bot name: " + discordBotName); } void cleanMeUp { if (discord != null) pcall { print("Shutting down discord"); discord.shutdown(); print("Bot shut down"); } discord = null; } O userConcept(User user) { S crud = dm_gazelle_linesCRUD(); O userConcept = dm_call(crud, 'uniqUser, user.getIdLong()); dm_call(crud, 'cset, userConcept, litobjectarray(name := user.getName())); ret userConcept; } // API MessageChannel getChannel(long channelID) { ret discord.getTextChannelById(channelID); } void postInChannel(long channelID, S msg) { postInChannel(channelID, msg, null); } void postInChannel(long channelID, S msg, IVF1 onPost) { if (channelID == 0) ret; postInChannel(getChannel(channelID), msg, onPost); } void uploadFileInChannel(long channelID, File file, S fileName, S msg, IVF1 onPost) { if (channelID == 0) ret; uploadFileInChannel(getChannel(channelID), file, fileName, msg, onPost); } void postInChannel(MessageChannel channel, S msg, IVF1 onPost) { S originalMsg = msg; msg = shortenForDiscord(msg); if (empty(msg)) ret; S postID = !sendToPostedLinesCRUD ? null : (S) dm_pcall(gazelle_postedLinesCRUD(), 'postingLine, channel.getId(), msg); print("Posting in channel " + channel + ": " + msg); pcallFAll(onPostedInChannel, channel, msg); MessageAction a = channel.sendMessage(msg); if (msg != originalMsg) a.addFile(toUtf8(originalMsg), genericTextFileName()); a.queue(m -> msgPosted(m, onPost, postID), onQueueError); } S genericTextFileName() { ret ymd_minus_hms() + ".txt"; } void msgPosted(Message m, IVF1 onPost, S postID) enter { if (nempty(postID)) dm_call(gazelle_postedLinesCRUD(), 'donePosting, postID, "discord msg " + m.getId()); long msgId = m.getIdLong(); print("I sent msg: " + msgId); pcallF(onPost, m); } void postInChannel(S channel, S msg) { long id = dm_discord_channelID(channel); if (id == 0) fail("Channel not found: " + channel); postInChannel(id, msg); } void postInChannel(MessageChannel channel, S msg) { postInChannel(channel, msg, null); } void postImage(Map msgMap, S url) { postImage(msgMap, url, ""); } void postImage(Map msgMap, S url, S description) { postImage((MessageChannel) get channel(msgMap), url, description); } void postImage(MessageChannel channel, S url, S description) { print("Posting image: " + url); channel.sendMessage( new EmbedBuilder() .setImage(absoluteURL(url)) //.setTitle(unnull(title)) .setDescription(unnull(description)) .setColor(discordImageEmbedMysteriousLineColor) .build()).queue(null, onQueueError); // TODO: posted lines } void editMessage(long channelID, long msgID, S text) { getChannel(channelID).editMessageById(str(msgID), text).queue(null, onQueueError); } void sendPM(long userID, S _text) { S text = shortenForDiscord(_text); if (empty(text)) ret; print("Sending PM to " + userID + ": " + text); discord.getUserById(userID).openPrivateChannel().queue(channel -> { channel.sendMessage(text).queue(null, onQueueError); }, onQueueError); print("PM queued"); } void reply(Map msgMap, S text) { reply(msgMap, text, null); } void reply(Map msgMap, S text, O onPost) { if (empty(text = trim(text))) ret; postInChannel((MessageChannel) msgMap.get('channel), text, toIVF1(onPost)); } void iAmTyping(Map msgMap) pcall { ((MessageChannel) msgMap.get('channel)).sendTyping().queue(null, onQueueError); } S startKeepAliveModule() enter { ret dm_discord_startKeepAliveModule(module()); } bool reactToUser(User user) { bool bot = user.isBot(); // don't react to other bots if (bot && !reactToBots) false; // don't read msgs from myself if (user.getIdLong() == discordBotID) false; true; } void uploadFileInChannel(MessageChannel channel, File file, S fileName, S msg, IVF1 onPost) { msg = shortenForDiscord(msg); MessageAction a = empty(msg) ? channel.sendFile(file, fileName) : channel.sendMessage(msg).addFile(file, fileName); a.queue(m -> msgPosted(m, onPost, null), onQueueError); } void uploadFileInChannel(MessageChannel channel, byte[] data, S fileName, S msg, IVF1 onPost) { msg = shortenForDiscord(msg); MessageAction a = empty(msg) ? channel.sendFile(data, fileName) : channel.sendMessage(msg).addFile(data, fileName); a.queue(m -> msgPosted(m, onPost, null), onQueueError); } void uploadFileInChannel(long channelID, byte[] data, S fileName, S msg, IVF1 onPost) { uploadFileInChannel(getChannel(channelID), data, fileName, msg, onPost); } void queue(RestAction action) { action.queue(null, onQueueError); }