Add jump urls to channel mentions & rendering messages, handle translatable components when serializing to plain

This commit is contained in:
Vankka 2023-05-19 19:59:03 +03:00
parent 2e2c1667eb
commit 2d4252ed6f
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
17 changed files with 249 additions and 27 deletions

View File

@ -23,6 +23,7 @@
package com.discordsrv.api.component;
import com.discordsrv.api.placeholder.provider.SinglePlaceholder;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
@ -38,6 +39,14 @@ public interface GameTextBuilder {
@NotNull
GameTextBuilder addContext(Object... context);
default GameTextBuilder addPlaceholder(String placeholder, Object replacement) {
return addContext(new SinglePlaceholder(placeholder, replacement));
}
default GameTextBuilder addPlaceholder(String placeholder, Supplier<Object> replacementSupplier) {
return addContext(new SinglePlaceholder(placeholder, replacementSupplier));
}
@NotNull
default GameTextBuilder addReplacement(String target, Object replacement) {
return addReplacement(Pattern.compile(target, Pattern.LITERAL), replacement);

View File

@ -70,6 +70,23 @@ public interface DiscordAPI {
@Nullable
DiscordTextChannel getTextChannelById(long id);
/**
* Gets a Discord voice channel by id, the provided entity should be stored for long periods of time.
* @param id the id for the voice channel
* @return the voice channel
*/
@Nullable
DiscordVoiceChannel getVoiceChannelById(long id);
/**
* Gets a Discord stage channel by id, the provided entity should be stored for long periods of time.
* @param id the id for the voice channel
* @return the voice channel
*/
@Nullable
DiscordStageChannel getStageChannelById(long id);
/**
* Gets a Discord thread channel by id from the cache, the provided entity should not be stored for long periods of time.
* @param id the id for the thread channel

View File

@ -25,6 +25,7 @@ package com.discordsrv.api.discord.entity.channel;
import com.discordsrv.api.discord.entity.Snowflake;
import com.discordsrv.api.discord.entity.guild.DiscordGuild;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import org.jetbrains.annotations.NotNull;
public interface DiscordGuildChannel extends Snowflake {
@ -34,6 +35,7 @@ public interface DiscordGuildChannel extends Snowflake {
* @return the name of the channel
*/
@NotNull
@Placeholder("channel_name")
String getName();
/**
@ -42,4 +44,12 @@ public interface DiscordGuildChannel extends Snowflake {
*/
@NotNull
DiscordGuild getGuild();
/**
* Gets the jump url for this channel
* @return the https url to go to this channel
*/
@NotNull
@Placeholder("channel_jump_url")
String getJumpUrl();
}

View File

@ -0,0 +1,12 @@
package com.discordsrv.api.discord.entity.channel;
import com.discordsrv.api.discord.entity.JDAEntity;
import net.dv8tion.jda.api.entities.channel.concrete.StageChannel;
public interface DiscordStageChannel extends DiscordGuildMessageChannel, JDAEntity<StageChannel> {
@Override
default DiscordChannelType getType() {
return DiscordChannelType.STAGE;
}
}

View File

@ -0,0 +1,12 @@
package com.discordsrv.api.discord.entity.channel;
import com.discordsrv.api.discord.entity.JDAEntity;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
public interface DiscordVoiceChannel extends DiscordGuildMessageChannel, JDAEntity<VoiceChannel> {
@Override
default DiscordChannelType getType() {
return DiscordChannelType.VOICE;
}
}

View File

@ -29,6 +29,9 @@ import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer;
import dev.vankka.mcdiscordreserializer.discord.DiscordSerializerOptions;
import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializer;
import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializerOptions;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
@ -48,6 +51,7 @@ public class ComponentFactory implements MinecraftComponentFactory {
private final DiscordSRV discordSRV;
private final MinecraftSerializer minecraftSerializer;
private final DiscordSerializer discordSerializer;
private final PlainTextComponentSerializer plainSerializer;
// Not the same as Adventure's TranslationRegistry
private final TranslationRegistry translationRegistry = new TranslationRegistry();
@ -61,20 +65,29 @@ public class ComponentFactory implements MinecraftComponentFactory {
this.discordSerializer = new DiscordSerializer();
discordSerializer.setDefaultOptions(
DiscordSerializerOptions.defaults()
.withTranslationProvider(translatableComponent -> {
Translation translation = translationRegistry.lookup(Locale.US, translatableComponent.key());
if (translation == null) {
return null;
}
.withTranslationProvider(this::provideTranslation)
);
this.plainSerializer = PlainTextComponentSerializer.builder()
.flattener(
ComponentFlattener.basic().toBuilder()
.mapper(TranslatableComponent.class, this::provideTranslation)
.build()
)
.build();
}
return translation.translate(
translatableComponent.args()
.stream()
.map(discordSerializer::serialize)
.map(str -> (Object) str)
.toArray(Object[]::new)
);
})
private String provideTranslation(TranslatableComponent component) {
Translation translation = translationRegistry.lookup(Locale.US, component.key());
if (translation == null) {
return null;
}
return translation.translate(
component.args()
.stream()
.map(discordSerializer::serialize)
.map(str -> (Object) str)
.toArray(Object[]::new)
);
}
@ -96,6 +109,10 @@ public class ComponentFactory implements MinecraftComponentFactory {
return discordSerializer;
}
public PlainTextComponentSerializer plainSerializer() {
return plainSerializer;
}
public TranslationRegistry translationRegistry() {
return translationRegistry;
}

View File

@ -33,12 +33,16 @@ import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.utils.MiscUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import org.jetbrains.annotations.NotNull;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
private static final Pattern MESSAGE_URL_PATTERN = Pattern.compile("https://(?:(?:ptb|canary)\\.)?discord\\.com/channels/[0-9]{16,20}/([0-9]{16,20})/[0-9]{16,20}");
private static final ThreadLocal<Context> CONTEXT = new ThreadLocal<>();
private final DiscordSRV discordSRV;
@ -69,6 +73,41 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
return output;
}
@Override
public Component appendLink(@NotNull Component part, String link) {
JDA jda = discordSRV.jda();
if (jda != null) {
Matcher matcher = MESSAGE_URL_PATTERN.matcher(link);
if (matcher.matches()) {
String channel = matcher.group(1);
GuildChannel guildChannel = jda.getGuildChannelById(channel);
Context context = CONTEXT.get();
String format = context != null ? context.config.map(cfg -> cfg.mentions).get(cfg -> cfg.message) : null;
if (format == null || guildChannel == null) {
return super.appendLink(part, link);
}
return Component.text()
.clickEvent(ClickEvent.openUrl(link))
.append(
ComponentUtil.fromAPI(
discordSRV.componentFactory()
.textBuilder(format)
.addContext(guildChannel)
.addPlaceholder("jump_url", link)
.applyPlaceholderService()
.build()
)
)
.build();
}
}
return super.appendLink(part, link);
}
@Override
public @NotNull Component appendChannelMention(@NotNull Component component, @NotNull String id) {
Context context = CONTEXT.get();
@ -88,7 +127,7 @@ public class DiscordSRVMinecraftRenderer extends DefaultMinecraftRenderer {
return component.append(ComponentUtil.fromAPI(
discordSRV.componentFactory()
.textBuilder(guildChannel != null ? format.format : format.unknownFormat)
.addReplacement("%channel_name%", guildChannel != null ? guildChannel.getName() : null)
.addContext(guildChannel)
.applyPlaceholderService()
.build()
));

View File

@ -60,9 +60,11 @@ public class DiscordToMinecraftChatConfig {
public static class Mentions {
public Format role = new Format("&#5865f2@%role_name%", "&#5865f2@deleted-role");
public Format channel = new Format("&#5865f2#%channel_name%", "&#5865f2#deleted-channel");
public Format channel = new Format("[hover:show_text:Click to go to channel][click:open_url:%channel_jump_url%]&#5865f2#%channel_name%", "&#5865f2#deleted-channel");
public Format user = new Format("[hover:show_text:Tag: %user_tag%&r\nRoles: %user_roles_, |text_&7&oNone%]&#5865f2@%user_effective_name|user_name%", "&#5865f2@Unknown user");
public String message = "[hover:show_text:Click to go to message][click:open_url:%jump_url%]&#5865f2#%channel_name% > ...";
@ConfigSerializable
public static class Format {

View File

@ -48,10 +48,7 @@ import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Expiry;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.concrete.NewsChannel;
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
import net.dv8tion.jda.api.entities.channel.concrete.*;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.requests.ErrorResponse;
@ -397,6 +394,24 @@ public class DiscordAPIImpl implements DiscordAPI {
return new DiscordTextChannelImpl(discordSRV, jda);
}
@Override
public @Nullable DiscordVoiceChannel getVoiceChannelById(long id) {
return mapJDAEntity(jda -> jda.getVoiceChannelById(id), this::getVoiceChannel);
}
public DiscordVoiceChannelImpl getVoiceChannel(VoiceChannel jda) {
return new DiscordVoiceChannelImpl(discordSRV, jda);
}
@Override
public @Nullable DiscordStageChannel getStageChannelById(long id) {
return mapJDAEntity(jda -> jda.getStageChannelById(id), this::getStageChannel);
}
public DiscordStageChannelImpl getStageChannel(StageChannel jda) {
return new DiscordStageChannelImpl(discordSRV, jda);
}
@Override
public @Nullable DiscordThreadChannel getCachedThreadChannelById(long id) {
return mapJDAEntity(jda -> jda.getThreadChannelById(id), this::getThreadChannel);

View File

@ -65,6 +65,11 @@ public abstract class AbstractDiscordGuildMessageChannel<T extends GuildMessageC
return channel.getAsMention();
}
@Override
public @NotNull String getJumpUrl() {
return channel.getJumpUrl();
}
@Override
public @NotNull DiscordGuild getGuild() {
return guild;

View File

@ -0,0 +1,40 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2023 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.entity.channel.DiscordStageChannel;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.channel.concrete.StageChannel;
public class DiscordStageChannelImpl extends AbstractDiscordGuildMessageChannel<StageChannel> implements DiscordStageChannel {
public DiscordStageChannelImpl(DiscordSRV discordSRV, StageChannel stageChannel) {
super(discordSRV, stageChannel);
}
@Override
public StageChannel asJDA() {
return channel;
}
@Override
public String toString() {
return "StageChannel:" + getName() + "(" + Long.toUnsignedString(getId()) + ")";
}
}

View File

@ -0,0 +1,40 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2023 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.entity.channel.DiscordVoiceChannel;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
public class DiscordVoiceChannelImpl extends AbstractDiscordGuildMessageChannel<VoiceChannel> implements DiscordVoiceChannel {
public DiscordVoiceChannelImpl(DiscordSRV discordSRV, VoiceChannel voiceChannel) {
super(discordSRV, voiceChannel);
}
@Override
public VoiceChannel asJDA() {
return channel;
}
@Override
public String toString() {
return "VoiceChannel:" + getName() + "(" + Long.toUnsignedString(getId()) + ")";
}
}

View File

@ -336,8 +336,8 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
for (Attachment attachment : attachments) {
components.add(ComponentUtil.fromAPI(
discordSRV.componentFactory().textBuilder(attachmentFormat)
.addReplacement("%file_name%", attachment.fileName())
.addReplacement("%file_url%", attachment.url())
.addPlaceholder("file_name", attachment.fileName())
.addPlaceholder("file_url", attachment.url())
.build()
));
}

View File

@ -50,8 +50,7 @@ import com.neovisionaries.ws.client.WebSocketFrame;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.*;
import net.dv8tion.jda.api.events.StatusChangeEvent;
import net.dv8tion.jda.api.events.session.SessionDisconnectEvent;
import net.dv8tion.jda.api.events.session.ShutdownEvent;
@ -236,6 +235,12 @@ public class JDAConnectionManager implements DiscordConnectionManager {
converted = api().getDirectMessageChannel((PrivateChannel) o);
} else if (o instanceof TextChannel) {
converted = api().getTextChannel((TextChannel) o);
} else if (o instanceof ThreadChannel) {
converted = api().getThreadChannel((ThreadChannel) o);
} else if (o instanceof VoiceChannel) {
converted = api().getVoiceChannel((VoiceChannel) o);
} else if (o instanceof StageChannel) {
converted = api().getStageChannel((StageChannel) o);
} else if (o instanceof Guild) {
converted = api().getGuild((Guild) o);
} else if (o instanceof Member) {

View File

@ -129,7 +129,7 @@ public class DiscordChatMessageModule extends AbstractModule<DiscordSRV> {
GameTextBuilder componentBuilder = discordSRV.componentFactory()
.textBuilder(format)
.addContext(discordMessage, author, channel, channelConfig)
.addReplacement("%message%", messageComponent);
.addPlaceholder("message", messageComponent);
if (member != null) {
componentBuilder.addContext(member);
}

View File

@ -25,7 +25,6 @@ import com.discordsrv.api.placeholder.mapper.PlaceholderResultMapper;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.NotNull;
public class ComponentResultStringifier implements PlaceholderResultMapper {
@ -47,7 +46,7 @@ public class ComponentResultStringifier implements PlaceholderResultMapper {
switch (mappingState) {
case ANSI: // TODO: ansi serializer (?)
case PLAIN:
return PlainTextComponentSerializer.plainText().serialize(component);
return discordSRV.componentFactory().plainSerializer().serialize(component);
default:
case NORMAL:
return new FormattedText(discordSRV.componentFactory().discordSerializer().serialize(component));

View File

@ -130,7 +130,7 @@ dependencyResolutionManagement {
library('adventure-serializer-bungee', 'net.kyori', 'adventure-text-serializer-bungeecord').versionRef('adventure-platform')
// MCDiscordReserializer & EnhancedLegacyText
library('mcdiscordreserializer', 'dev.vankka', 'mcdiscordreserializer').version('4.3.0')
library('mcdiscordreserializer', 'dev.vankka', 'mcdiscordreserializer').version('4.4.0-SNAPSHOT')
library('enhancedlegacytext', 'dev.vankka', 'enhancedlegacytext').version('1.0.0')
// JUnit