Add message mirroring

This commit is contained in:
Vankka 2022-01-13 19:00:24 +02:00
parent 047580d9cf
commit 0c9732fe11
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
11 changed files with 607 additions and 85 deletions

View File

@ -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<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message);
CompletableFuture<ReceivedDiscordMessage> 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<Void> deleteMessageById(long id);
CompletableFuture<Void> 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<ReceivedDiscordMessage> editMessageById(long id, SendableDiscordMessage message);
CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, @NotNull SendableDiscordMessage message);
/**
* Returns the JDA representation of this object. This should not be used if it can be avoided.

View File

@ -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<DiscordTextChannel> getTextChannel() {
return channel instanceof DiscordTextChannel
? Optional.of((DiscordTextChannel) channel)
: Optional.empty();
? Optional.of((DiscordTextChannel) channel)
: Optional.empty();
}
@NotNull
public Optional<DiscordThreadChannel> getThreadChannel() {
return channel instanceof DiscordThreadChannel
? Optional.of((DiscordThreadChannel) channel)
: Optional.empty();
}
@NotNull
public Optional<DiscordDMChannel> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Long> 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);
}
}

View File

@ -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<Pattern, String> 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<Long> 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();

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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();
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T extends MessageChannel>
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;
}
}

View File

@ -16,49 +16,49 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<DiscordGuildMember> 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();

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<MessageReference, Set<MessageReference>> 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<GameChannel, OrDefault<BaseChannelConfig>> channels = discordSRV.channelConfig().orDefault(event.getChannel());
if (channels == null || channels.isEmpty()) {
return;
}
ReceivedDiscordMessage message = event.getDiscordMessage();
DiscordMessageChannel channel = event.getChannel();
List<DiscordMessageChannel> mirrorChannels = new ArrayList<>();
List<CompletableFuture<DiscordThreadChannel>> futures = new ArrayList<>();
for (Map.Entry<GameChannel, OrDefault<BaseChannelConfig>> entry : channels.entrySet()) {
OrDefault<BaseChannelConfig> channelConfig = entry.getValue();
OrDefault<MirroringConfig> 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<Long> 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<DiscordTextChannel> text = new ArrayList<>();
List<DiscordThreadChannel> 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<CompletableFuture<ReceivedDiscordMessage>> 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<MessageReference> messages = new HashSet<>();
for (CompletableFuture<ReceivedDiscordMessage> 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<MessageReference> references = mapping.get(getReference(message), k -> null);
if (references == null) {
return;
}
Map<DiscordTextChannel, MessageReference> text = new LinkedHashMap<>();
Map<DiscordThreadChannel, MessageReference> 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<DiscordTextChannel, MessageReference> 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<DiscordThreadChannel, MessageReference> 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<MessageReference> 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);
}
}
}