diff --git a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java index 253b8cf6..47e0cebe 100644 --- a/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java +++ b/api/src/main/java/com/discordsrv/api/discord/api/entity/channel/DiscordMessageChannel.java @@ -45,15 +45,16 @@ public interface DiscordMessageChannel extends Snowflake { * @throws com.discordsrv.api.discord.api.exception.NotReadyException if DiscordSRV is not ready, {@link com.discordsrv.api.DiscordSRVApi#isReady()} */ @NotNull - CompletableFuture sendMessage(SendableDiscordMessage message); + CompletableFuture sendMessage(@NotNull SendableDiscordMessage message); /** * Deletes the message identified by the id. * * @param id the id of the message to delete + * @param webhookMessage if the message is a webhook message or not * @return a future that will fail if the request fails */ - CompletableFuture deleteMessageById(long id); + CompletableFuture deleteMessageById(long id, boolean webhookMessage); /** * Edits the message identified by the id. @@ -64,7 +65,7 @@ public interface DiscordMessageChannel extends Snowflake { * @throws com.discordsrv.api.discord.api.exception.NotReadyException if DiscordSRV is not ready, {@link com.discordsrv.api.DiscordSRVApi#isReady()} */ @NotNull - CompletableFuture editMessageById(long id, SendableDiscordMessage message); + CompletableFuture editMessageById(long id, @NotNull SendableDiscordMessage message); /** * Returns the JDA representation of this object. This should not be used if it can be avoided. diff --git a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageReceivedEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/AbstractDiscordMessageEvent.java similarity index 68% rename from api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageReceivedEvent.java rename to api/src/main/java/com/discordsrv/api/discord/events/AbstractDiscordMessageEvent.java index 3fd3133d..4abd3794 100644 --- a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageReceivedEvent.java +++ b/api/src/main/java/com/discordsrv/api/discord/events/AbstractDiscordMessageEvent.java @@ -26,46 +26,53 @@ package com.discordsrv.api.discord.events; import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; -import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; +import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel; import com.discordsrv.api.event.events.Event; import org.jetbrains.annotations.NotNull; import java.util.Optional; -public class DiscordMessageReceivedEvent implements Event { +public abstract class AbstractDiscordMessageEvent implements Event { - private final ReceivedDiscordMessage message; private final DiscordMessageChannel channel; - public DiscordMessageReceivedEvent(ReceivedDiscordMessage message, DiscordMessageChannel channel) { - this.message = message; + public AbstractDiscordMessageEvent(DiscordMessageChannel channel) { this.channel = channel; } public boolean isGuildMessage() { - return getTextChannel().isPresent(); + return !getDMChannel().isPresent(); } + /** + * The Discord text channel if this event originated from a message sent in a text channel. + * This will not be present on messages from threads (see {@link #getThreadChannel()}). + * @return an optional potentially containing a {@link DiscordTextChannel} + */ @NotNull public Optional getTextChannel() { return channel instanceof DiscordTextChannel - ? Optional.of((DiscordTextChannel) channel) - : Optional.empty(); + ? Optional.of((DiscordTextChannel) channel) + : Optional.empty(); + } + + @NotNull + public Optional getThreadChannel() { + return channel instanceof DiscordThreadChannel + ? Optional.of((DiscordThreadChannel) channel) + : Optional.empty(); } @NotNull public Optional getDMChannel() { return channel instanceof DiscordDMChannel - ? Optional.of((DiscordDMChannel) channel) - : Optional.empty(); + ? Optional.of((DiscordDMChannel) channel) + : Optional.empty(); } @NotNull public DiscordMessageChannel getChannel() { - return channel ; + return channel; } - public ReceivedDiscordMessage getMessage() { - return message; - } } diff --git a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageDeleteEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageDeleteEvent.java new file mode 100644 index 00000000..8ac5f532 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageDeleteEvent.java @@ -0,0 +1,40 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.events; + +import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; + +public class DiscordMessageDeleteEvent extends AbstractDiscordMessageEvent { + + private final long messageId; + + public DiscordMessageDeleteEvent(DiscordMessageChannel channel, long messageId) { + super(channel); + this.messageId = messageId; + } + + public long getMessageId() { + return messageId; + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageUpdateEvent.java b/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageUpdateEvent.java new file mode 100644 index 00000000..1f110988 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/events/DiscordMessageUpdateEvent.java @@ -0,0 +1,42 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.events; + +import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; +import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; + +public class DiscordMessageUpdateEvent extends AbstractDiscordMessageEvent { + + private final ReceivedDiscordMessage message; + + public DiscordMessageUpdateEvent(DiscordMessageChannel channel, ReceivedDiscordMessage message) { + super(channel); + this.message = message; + } + + public ReceivedDiscordMessage getMessage() { + return message; + } +} + diff --git a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageProcessingEvent.java b/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageProcessingEvent.java index b7e98c33..09d1c46b 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageProcessingEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/message/receive/discord/DiscordChatMessageProcessingEvent.java @@ -23,7 +23,9 @@ package com.discordsrv.api.event.events.message.receive.discord; +import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; +import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuild; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.event.events.Cancellable; @@ -34,14 +36,17 @@ public class DiscordChatMessageProcessingEvent implements Cancellable, Processab private final ReceivedDiscordMessage discordMessage; private String messageContent; - private final DiscordTextChannel channel; + private final DiscordMessageChannel channel; private boolean cancelled; private boolean processed; - public DiscordChatMessageProcessingEvent(@NotNull ReceivedDiscordMessage discordMessage, @NotNull DiscordTextChannel channel) { + public DiscordChatMessageProcessingEvent(@NotNull ReceivedDiscordMessage discordMessage, @NotNull DiscordMessageChannel channel) { this.discordMessage = discordMessage; this.messageContent = discordMessage.getContent().orElse(null); this.channel = channel; + if (!(channel instanceof DiscordTextChannel) && !(channel instanceof DiscordThreadChannel)) { + throw new IllegalStateException("Cannot process messages that aren't from a text channel or thread"); + } } public ReceivedDiscordMessage getDiscordMessage() { @@ -56,12 +61,18 @@ public class DiscordChatMessageProcessingEvent implements Cancellable, Processab this.messageContent = messageContent; } - public DiscordTextChannel getChannel() { + public DiscordMessageChannel getChannel() { return channel; } public DiscordGuild getGuild() { - return channel.getGuild(); + if (channel instanceof DiscordTextChannel) { + return ((DiscordTextChannel) channel).getGuild(); + } else if (channel instanceof DiscordThreadChannel) { + return ((DiscordThreadChannel) channel).getParentChannel().getGuild(); + } else { + throw new IllegalStateException("Message isn't from a text channel or thread"); + } } @Override diff --git a/common/src/main/java/com/discordsrv/common/config/main/DiscordIgnores.java b/common/src/main/java/com/discordsrv/common/config/main/DiscordIgnores.java new file mode 100644 index 00000000..88b7472a --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/DiscordIgnores.java @@ -0,0 +1,72 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.main; + +import com.discordsrv.api.discord.api.entity.DiscordUser; +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@ConfigSerializable +public class DiscordIgnores { + + @Comment("User, bot and webhook ids to ignore") + public IDs usersAndWebhookIds = new IDs(); + + @Comment("Role ids for users/bots to ignore") + public IDs roleIds = new IDs(); + + @Comment("If bots (webhooks not included) should be ignored") + public boolean bots = false; + + @Comment("If webhooks should be ignored") + public boolean webhooks = true; + + @ConfigSerializable + public static class IDs { + + public List ids = new ArrayList<>(); + + @Comment("true for whitelisting the provided ids, false for blacklisting them") + public boolean whitelist = false; + } + + public boolean shouldBeIgnored(boolean webhookMessage, DiscordUser author, DiscordGuildMember member) { + if (webhooks && webhookMessage) { + return true; + } else if (bots && (author.isBot() && !webhookMessage)) { + return true; + } + + DiscordIgnores.IDs users = usersAndWebhookIds; + if (users != null && users.ids.contains(author.getId()) != users.whitelist) { + return true; + } + + DiscordIgnores.IDs roles = roleIds; + return roles != null && Optional.ofNullable(member) + .map(m -> m.getRoles().stream().anyMatch(role -> roles.ids.contains(role.getId()))) + .map(hasRole -> hasRole != roles.whitelist) + .orElse(false); + } +} diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java index eb944e2b..4f0280b0 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/DiscordToMinecraftChatConfig.java @@ -19,12 +19,11 @@ package com.discordsrv.common.config.main.channels; import com.discordsrv.common.config.annotation.Untranslated; +import com.discordsrv.common.config.main.DiscordIgnores; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; -import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -44,7 +43,7 @@ public class DiscordToMinecraftChatConfig { @Comment("Attachment format") @Untranslated(Untranslated.Type.VALUE) - public String attachmentFormat = "[hover:show_text:Open %file_name% in browser][click:open_url:%file_url%]&a[&f%file_name%&a]&r"; + public String attachmentFormat = " [hover:show_text:Open %file_name% in browser][click:open_url:%file_url%]&a[&f%file_name%&a]&r"; // TODO: more info on regex pairs (String#replaceAll) @Comment("Regex filters for Discord message contents (this is the %message% part of the \"format\" option)") @@ -52,32 +51,7 @@ public class DiscordToMinecraftChatConfig { public Map contentRegexFilters = new LinkedHashMap<>(); @Comment("Users, bots and webhooks to ignore") - public Ignores ignores = new Ignores(); - - @ConfigSerializable - public static class Ignores { - - @Comment("User, bot and webhook ids to ignore") - public IDs usersAndWebhookIds = new IDs(); - - @Comment("Role ids for users/bots to ignore") - public IDs roleIds = new IDs(); - - @Comment("If bots (webhooks not included) should be ignored") - public boolean bots = false; - - @Comment("If webhooks should be ignored") - public boolean webhooks = true; - - @ConfigSerializable - public static class IDs { - - public List ids = new ArrayList<>(); - - @Comment("true for whitelisting the provided ids, false for blacklisting them") - public boolean whitelist = false; - } - } + public DiscordIgnores ignores = new DiscordIgnores(); @Comment("The representations of Discord mentions in-game") public Mentions mentions = new Mentions(); diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/MirroringConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/MirroringConfig.java new file mode 100644 index 00000000..f64a4114 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/MirroringConfig.java @@ -0,0 +1,32 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.main.channels; + +import com.discordsrv.common.config.main.DiscordIgnores; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class MirroringConfig { + + public boolean enabled = true; + + @Comment("Users, bots and webhooks to ignore when mirroring") + public DiscordIgnores ignores = new DiscordIgnores(); +} diff --git a/common/src/main/java/com/discordsrv/common/discord/api/entity/channel/DiscordMessageChannelImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/entity/channel/DiscordMessageChannelImpl.java new file mode 100644 index 00000000..eafcf9a7 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/discord/api/entity/channel/DiscordMessageChannelImpl.java @@ -0,0 +1,59 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.discord.api.entity.channel; + +import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; +import com.discordsrv.common.DiscordSRV; +import net.dv8tion.jda.api.entities.*; + +public abstract class DiscordMessageChannelImpl + implements DiscordMessageChannel { + + public static DiscordMessageChannelImpl get(DiscordSRV discordSRV, MessageChannel messageChannel) { + if (messageChannel instanceof TextChannel) { + return new DiscordTextChannelImpl(discordSRV, (TextChannel) messageChannel); + } else if (messageChannel instanceof ThreadChannel) { + return new DiscordThreadChannelImpl(discordSRV, (ThreadChannel) messageChannel); + } else if (messageChannel instanceof PrivateChannel) { + return new DiscordDMChannelImpl(discordSRV, (PrivateChannel) messageChannel); + } else if (messageChannel instanceof NewsChannel) { + return new DiscordNewsChannelImpl(discordSRV, (NewsChannel) messageChannel); + } else { + throw new IllegalArgumentException("Unmappable MessageChannel type: " + messageChannel.getClass().getName()); + } + } + + protected final DiscordSRV discordSRV; + protected final T channel; + + public DiscordMessageChannelImpl(DiscordSRV discordSRV, T channel) { + this.discordSRV = discordSRV; + this.channel = channel; + } + + @Override + public long getId() { + return channel.getIdLong(); + } + + @Override + public MessageChannel getAsJDAMessageChannel() { + return channel; + } +} diff --git a/common/src/main/java/com/discordsrv/common/module/modules/message/DiscordToMinecraftChatModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java similarity index 70% rename from common/src/main/java/com/discordsrv/common/module/modules/message/DiscordToMinecraftChatModule.java rename to common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java index dcb08ddc..05a25155 100644 --- a/common/src/main/java/com/discordsrv/common/module/modules/message/DiscordToMinecraftChatModule.java +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordChatMessageModule.java @@ -16,49 +16,49 @@ * along with this program. If not, see . */ -package com.discordsrv.common.module.modules.message; +package com.discordsrv.common.messageforwarding.discord; import com.discordsrv.api.channel.GameChannel; import com.discordsrv.api.component.EnhancedTextBuilder; import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.discord.api.entity.DiscordUser; -import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; +import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; -import com.discordsrv.api.discord.events.DiscordMessageReceivedEvent; +import com.discordsrv.api.discord.events.DiscordMessageReceiveEvent; import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageProcessingEvent; import com.discordsrv.api.placeholder.util.Placeholders; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.component.renderer.DiscordSRVMinecraftRenderer; import com.discordsrv.common.component.util.ComponentUtil; -import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; +import com.discordsrv.common.config.main.DiscordIgnores; import com.discordsrv.common.config.main.channels.DiscordToMinecraftChatConfig; +import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.function.OrDefault; import com.discordsrv.common.module.type.AbstractModule; import net.kyori.adventure.text.Component; import java.util.Map; -import java.util.Optional; -public class DiscordToMinecraftChatModule extends AbstractModule { +public class DiscordChatMessageModule extends AbstractModule { - public DiscordToMinecraftChatModule(DiscordSRV discordSRV) { + public DiscordChatMessageModule(DiscordSRV discordSRV) { super(discordSRV); } @Subscribe - public void onGuildMessageReceived(DiscordMessageReceivedEvent event) { - DiscordTextChannel channel = event.getTextChannel().orElse(null); - if (channel == null || !discordSRV.isReady() || event.getMessage().isFromSelf()) { + public void onDiscordMessageReceived(DiscordMessageReceiveEvent event) { + if (!discordSRV.isReady() || event.getMessage().isFromSelf() + || !(event.getTextChannel().isPresent() || event.getThreadChannel().isPresent())) { return; } - discordSRV.eventBus().publish(new DiscordChatMessageProcessingEvent(event.getMessage(), channel)); + discordSRV.eventBus().publish(new DiscordChatMessageProcessingEvent(event.getMessage(), event.getChannel())); } @Subscribe - public void onDiscordMessageReceive(DiscordChatMessageProcessingEvent event) { + public void onDiscordChatMessageProcessing(DiscordChatMessageProcessingEvent event) { if (checkCancellation(event) || checkProcessor(event)) { return; } @@ -80,32 +80,16 @@ public class DiscordToMinecraftChatModule extends AbstractModule { return; } - DiscordTextChannel channel = event.getChannel(); + DiscordMessageChannel channel = event.getChannel(); ReceivedDiscordMessage discordMessage = event.getDiscordMessage(); DiscordUser author = discordMessage.getAuthor(); - Optional member = discordMessage.getMember(); + DiscordGuildMember member = discordMessage.getMember().orElse(null); boolean webhookMessage = discordMessage.isWebhookMessage(); - DiscordToMinecraftChatConfig.Ignores ignores = chatConfig.get(cfg -> cfg.ignores); - if (ignores != null) { - if (ignores.webhooks && webhookMessage) { - return; - } else if (ignores.bots && (author.isBot() && !webhookMessage)) { - return; - } - - DiscordToMinecraftChatConfig.Ignores.IDs users = ignores.usersAndWebhookIds; - if (users != null && users.ids.contains(author.getId()) != users.whitelist) { - return; - } - - DiscordToMinecraftChatConfig.Ignores.IDs roles = ignores.roleIds; - if (roles != null && member - .map(m -> m.getRoles().stream().anyMatch(role -> roles.ids.contains(role.getId()))) - .map(hasRole -> hasRole != roles.whitelist) - .orElse(false)) { - return; - } + DiscordIgnores ignores = chatConfig.get(cfg -> cfg.ignores); + if (ignores != null && ignores.shouldBeIgnored(webhookMessage, author, member)) { + // TODO: response for humans + return; } String format = chatConfig.opt(cfg -> webhookMessage ? cfg.webhookFormat : cfg.format) @@ -126,7 +110,9 @@ public class DiscordToMinecraftChatModule extends AbstractModule { .enhancedBuilder(format) .addContext(discordMessage, author, channel, channelConfig) .addReplacement("%message%", messageComponent); - member.ifPresent(componentBuilder::addContext); + if (member != null) { + componentBuilder.addContext(member); + } componentBuilder.applyPlaceholderService(); diff --git a/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java new file mode 100644 index 00000000..8efecdb2 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/messageforwarding/discord/DiscordMessageMirroringModule.java @@ -0,0 +1,298 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.messageforwarding.discord; + +import com.discordsrv.api.channel.GameChannel; +import com.discordsrv.api.discord.api.entity.DiscordUser; +import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; +import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; +import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel; +import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember; +import com.discordsrv.api.discord.api.entity.message.DiscordMessageEmbed; +import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage; +import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage; +import com.discordsrv.api.discord.events.DiscordMessageDeleteEvent; +import com.discordsrv.api.discord.events.DiscordMessageUpdateEvent; +import com.discordsrv.api.event.bus.Subscribe; +import com.discordsrv.api.event.events.message.receive.discord.DiscordChatMessageProcessingEvent; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.config.main.DiscordIgnores; +import com.discordsrv.common.config.main.channels.MirroringConfig; +import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; +import com.discordsrv.common.config.main.channels.base.IChannelConfig; +import com.discordsrv.common.function.OrDefault; +import com.discordsrv.common.module.type.AbstractModule; +import com.github.benmanes.caffeine.cache.Cache; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class DiscordMessageMirroringModule extends AbstractModule { + + private final Cache> mapping; + + public DiscordMessageMirroringModule(DiscordSRV discordSRV) { + super(discordSRV); + this.mapping = discordSRV.caffeineBuilder() + .expireAfterWrite(30, TimeUnit.MINUTES) + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(); + } + + @Subscribe + public void onDiscordChatMessageProcessing(DiscordChatMessageProcessingEvent event) { + if (checkCancellation(event)) { + return; + } + + Map> channels = discordSRV.channelConfig().orDefault(event.getChannel()); + if (channels == null || channels.isEmpty()) { + return; + } + + ReceivedDiscordMessage message = event.getDiscordMessage(); + DiscordMessageChannel channel = event.getChannel(); + + List mirrorChannels = new ArrayList<>(); + List> futures = new ArrayList<>(); + + for (Map.Entry> entry : channels.entrySet()) { + OrDefault channelConfig = entry.getValue(); + OrDefault config = channelConfig.map(cfg -> cfg.mirroring); + if (!config.get(cfg -> cfg.enabled, true)) { + continue; + } + + DiscordIgnores ignores = config.get(cfg -> cfg.ignores); + if (ignores != null && ignores.shouldBeIgnored(message.isWebhookMessage(), message.getAuthor(), message.getMember().orElse(null))) { + continue; + } + + IChannelConfig iChannelConfig = channelConfig.get(cfg -> cfg instanceof IChannelConfig ? (IChannelConfig) cfg : null); + if (iChannelConfig == null) { + continue; + } + + List channelIds = iChannelConfig.channelIds(); + if (channelIds != null) { + for (Long channelId : channelIds) { + discordSRV.discordAPI().getTextChannelById(channelId).ifPresent(textChannel -> { + if (textChannel.getId() != channel.getId()) { + mirrorChannels.add(textChannel); + } + }); + } + } + + discordSRV.discordAPI().findOrCreateThreads(iChannelConfig, threadChannel -> { + if (threadChannel.getId() != channel.getId()) { + mirrorChannels.add(threadChannel); + } + }, futures); + } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).whenComplete((v, t) -> { + List text = new ArrayList<>(); + List thread = new ArrayList<>(); + for (DiscordMessageChannel mirrorChannel : mirrorChannels) { + if (mirrorChannel instanceof DiscordTextChannel) { + text.add((DiscordTextChannel) mirrorChannel); + } else if (mirrorChannel instanceof DiscordThreadChannel) { + thread.add((DiscordThreadChannel) mirrorChannel); + } + } + + SendableDiscordMessage.Builder builder = convert(event.getDiscordMessage()); + List> messageFutures = new ArrayList<>(); + if (!text.isEmpty()) { + SendableDiscordMessage finalMessage = builder.build(); + for (DiscordTextChannel textChannel : text) { + messageFutures.add(textChannel.sendMessage(finalMessage)); + } + } + if (!thread.isEmpty()) { + SendableDiscordMessage finalMessage = builder.convertToNonWebhook().build(); + for (DiscordThreadChannel threadChannel : thread) { + messageFutures.add(threadChannel.sendMessage(finalMessage)); + } + } + + CompletableFuture.allOf(messageFutures.toArray(new CompletableFuture[0])).whenComplete((v2, t2) -> { + Set messages = new HashSet<>(); + for (CompletableFuture messageFuture : messageFutures) { + if (messageFuture.isCompletedExceptionally()) { + continue; + } + + messages.add(getReference(messageFuture.join())); + } + + mapping.put(getReference(message), messages); + }); + }); + } + + @Subscribe + public void onDiscordMessageUpdate(DiscordMessageUpdateEvent event) { + ReceivedDiscordMessage message = event.getMessage(); + Set references = mapping.get(getReference(message), k -> null); + if (references == null) { + return; + } + + Map text = new LinkedHashMap<>(); + Map thread = new LinkedHashMap<>(); + for (MessageReference reference : references) { + DiscordMessageChannel channel = reference.getMessageChannel(discordSRV); + if (channel instanceof DiscordTextChannel) { + text.put((DiscordTextChannel) channel, reference); + } else if (channel instanceof DiscordThreadChannel) { + thread.put((DiscordThreadChannel) channel, reference); + } + } + SendableDiscordMessage.Builder builder = convert(message); + if (!text.isEmpty()) { + SendableDiscordMessage finalMessage = builder.build(); + for (Map.Entry entry : text.entrySet()) { + entry.getKey().editMessageById(entry.getValue().messageId, finalMessage).whenComplete((v, t) -> { + if (t != null) { + discordSRV.logger().error("Failed to update mirrored message in " + entry.getKey()); + } + }); + } + } + if (!thread.isEmpty()) { + SendableDiscordMessage finalMessage = builder.convertToNonWebhook().build(); + for (Map.Entry entry : thread.entrySet()) { + entry.getKey().editMessageById(entry.getValue().messageId, finalMessage).whenComplete((v, t) -> { + if (t != null) { + discordSRV.logger().error("Failed to update mirrored message in " + entry.getKey()); + } + }); + } + } + } + + @Subscribe + public void onDiscordMessageDelete(DiscordMessageDeleteEvent event) { + Set references = mapping.get(getReference(event.getChannel(), event.getMessageId(), false), k -> null); + if (references == null) { + return; + } + + for (MessageReference reference : references) { + DiscordMessageChannel channel = reference.getMessageChannel(discordSRV); + if (channel == null) { + continue; + } + + channel.deleteMessageById(reference.messageId, reference.webhookMessage).whenComplete((v, t) -> { + if (t != null) { + discordSRV.logger().error("Failed to delete mirrored message in " + channel); + } + }); + } + } + + private SendableDiscordMessage.Builder convert(ReceivedDiscordMessage message) { + DiscordGuildMember member = message.getMember().orElse(null); + DiscordUser user = message.getAuthor(); + + SendableDiscordMessage.Builder builder = SendableDiscordMessage.builder() + .setContent(message.getContent().orElse(null)) + .setWebhookUsername(member != null ? member.getEffectiveName() : user.getUsername()) + .setWebhookAvatarUrl(member != null + ? member.getEffectiveServerAvatarUrl() + : user.getEffectiveAvatarUrl()); + for (DiscordMessageEmbed embed : message.getEmbeds()) { + builder.addEmbed(embed); + } + return builder; + } + + private MessageReference getReference(ReceivedDiscordMessage message) { + return getReference(message.getChannel(), message.getId(), message.isWebhookMessage()); + } + + private MessageReference getReference(DiscordMessageChannel channel, long messageId, boolean webhookMessage) { + if (channel instanceof DiscordTextChannel) { + DiscordTextChannel textChannel = (DiscordTextChannel) channel; + return new MessageReference(textChannel, messageId, webhookMessage); + } else if (channel instanceof DiscordThreadChannel) { + DiscordThreadChannel threadChannel = (DiscordThreadChannel) channel; + return new MessageReference(threadChannel, messageId, webhookMessage); + } + throw new IllegalStateException("Unexpected channel type: " + channel.getClass().getName()); + } + + public static class MessageReference { + + private final long channelId; + private final long threadId; + private final long messageId; + private final boolean webhookMessage; + + public MessageReference(DiscordTextChannel textChannel, long messageId, boolean webhookMessage) { + this(textChannel.getId(), -1L, messageId, webhookMessage); + } + + public MessageReference(DiscordThreadChannel threadChannel, long messageId, boolean webhookMessage) { + this(threadChannel.getParentChannel().getId(), threadChannel.getId(), messageId, webhookMessage); + } + + public MessageReference(long channelId, long threadId, long messageId, boolean webhookMessage) { + this.channelId = channelId; + this.threadId = threadId; + this.messageId = messageId; + this.webhookMessage = webhookMessage; + } + + public DiscordMessageChannel getMessageChannel(DiscordSRV discordSRV) { + DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null); + if (textChannel == null) { + return null; + } else if (threadId == -1) { + return textChannel; + } + + for (DiscordThreadChannel activeThread : textChannel.getActiveThreads()) { + if (activeThread.getId() == threadId) { + return activeThread; + } + } + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MessageReference that = (MessageReference) o; + // Intentionally ignores webhookMessage + return channelId == that.channelId && threadId == that.threadId && messageId == that.messageId; + } + + @Override + public int hashCode() { + // Intentionally ignores webhookMessage + return Objects.hash(channelId, threadId, messageId); + } + } +}