mirror of
https://github.com/System-End/Discord-MC-Chat.git
synced 2026-04-19 22:05:11 +00:00
feat improve spoiler coverage edit layout and console parity logging
Co-authored-by: Xujiayao <58985541+Xujiayao@users.noreply.github.com>
This commit is contained in:
parent
d8e3554a04
commit
1bb5c52b1e
6 changed files with 151 additions and 18 deletions
|
|
@ -25,10 +25,12 @@ import com.xujiayao.discord_mc_chat.network.packets.commands.link.LinkResponsePa
|
|||
import com.xujiayao.discord_mc_chat.network.packets.commands.link.OpSyncPacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.commands.unlink.UnlinkResponsePacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.events.DiscordEventPacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.events.TextSegment;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.misc.KeepAlivePacket;
|
||||
import com.xujiayao.discord_mc_chat.network.packets.misc.LatencyPongPacket;
|
||||
import com.xujiayao.discord_mc_chat.utils.CryptUtils;
|
||||
import com.xujiayao.discord_mc_chat.utils.EnvironmentUtils;
|
||||
import com.xujiayao.discord_mc_chat.utils.config.ModeManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.events.CoreEvents;
|
||||
import com.xujiayao.discord_mc_chat.utils.events.EventManager;
|
||||
import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager;
|
||||
|
|
@ -202,6 +204,9 @@ public class ClientHandler extends SimpleChannelInboundHandler<Packet> {
|
|||
EventManager.post(new CoreEvents.OpSyncEvent(p.opLevels));
|
||||
case DiscordEventPacket p -> {
|
||||
// Handle Discord event forwarded from server - render in Minecraft
|
||||
if ("multi_server_client".equals(ModeManager.getMode())) {
|
||||
logDiscordEventForConsole(p);
|
||||
}
|
||||
switch (p.type) {
|
||||
case CHAT -> EventManager.post(new CoreEvents.DiscordChatMessageEvent(
|
||||
p.segments,
|
||||
|
|
@ -265,4 +270,16 @@ public class ClientHandler extends SimpleChannelInboundHandler<Packet> {
|
|||
}
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
private static void logDiscordEventForConsole(DiscordEventPacket p) {
|
||||
if (p.replySegments != null && !p.replySegments.isEmpty()) {
|
||||
LOGGER.info(TextSegment.toPlainText(p.replySegments));
|
||||
}
|
||||
if (p.segments != null && !p.segments.isEmpty()) {
|
||||
LOGGER.info(TextSegment.toPlainText(p.segments));
|
||||
}
|
||||
if (p.type == DiscordEventPacket.EventType.EDIT && p.editedMessageSegments != null && !p.editedMessageSegments.isEmpty()) {
|
||||
LOGGER.info(TextSegment.toPlainText(p.editedMessageSegments));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.xujiayao.discord_mc_chat.network.packets.events;
|
|||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a single rich text segment for in-game rendering.
|
||||
|
|
@ -92,4 +93,20 @@ public class TextSegment implements Serializable {
|
|||
this.bold = bold;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return text == null ? "" : text;
|
||||
}
|
||||
|
||||
public static String toPlainText(List<TextSegment> segments) {
|
||||
if (segments == null || segments.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (TextSegment segment : segments) {
|
||||
sb.append(segment == null ? "" : segment.toString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import java.util.Set;
|
|||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.xujiayao.discord_mc_chat.Constants.LOGGER;
|
||||
|
||||
/**
|
||||
* Handles Discord JDA events.
|
||||
*
|
||||
|
|
@ -409,6 +411,11 @@ public class DiscordEventHandler extends ListenerAdapter {
|
|||
packet.mentionedPlayerUuids = mentionedPlayerUuids;
|
||||
packet.mentionEveryone = isMentionEveryone;
|
||||
|
||||
if (replySegments != null && !replySegments.isEmpty()) {
|
||||
LOGGER.info(TextSegment.toPlainText(replySegments));
|
||||
}
|
||||
LOGGER.info(TextSegment.toPlainText(mainSegments));
|
||||
|
||||
NetworkManager.broadcastToClients(packet);
|
||||
|
||||
// Cache message for edit/delete reference
|
||||
|
|
@ -501,7 +508,7 @@ public class DiscordEventHandler extends ListenerAdapter {
|
|||
List<TextSegment> notificationSegments = DiscordMessageParser.buildEditNotificationSegments(editorName, roleColor);
|
||||
|
||||
// Build new message content segments
|
||||
List<TextSegment> editedMessageSegments = DiscordMessageParser.buildChatSegments(message);
|
||||
List<TextSegment> editedMessageSegments = DiscordMessageParser.buildEditedMessageSegments(message);
|
||||
|
||||
DiscordEventPacket packet = new DiscordEventPacket(DiscordEventPacket.EventType.EDIT, notificationSegments);
|
||||
packet.replySegments = replySegments;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ private static final Pattern BOLD_ITALIC_URL_PATTERN = Pattern.compile("\\*\\*\\
|
|||
private static final Pattern BOLD_URL_PATTERN = Pattern.compile("\\*\\*(https?://[^\\s*|~`<>)\\]]+)\\*\\*");
|
||||
private static final Pattern SPOILER_ITALIC_URL_PATTERN = Pattern.compile("\\|\\|\\*(https?://[^\\s*|~`<>)\\]]+)\\*\\|\\|");
|
||||
private static final Pattern SPOILER_URL_PATTERN = Pattern.compile("\\|\\|(https?://[^\\s*|~`<>)\\]]+)\\|\\|");
|
||||
private static final Pattern SPOILER_CONTENT_PATTERN = Pattern.compile("\\|\\|(.+?)\\|\\|");
|
||||
private static final Pattern LINK_TOKEN_PATTERN = Pattern.compile("\\[([^]]+)]\\((https?://[^)]+)\\)");
|
||||
private static final List<String> MARKDOWN_DELIMITERS = List.of("***", "~~", "||", "**", "__", "*", "_");
|
||||
|
||||
|
|
@ -344,6 +345,53 @@ segments.add(new TextSegment(text, bold, color));
|
|||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds segments for the edited message content line shown after edit notification.
|
||||
* <p>
|
||||
* Format follows the custom_messages {@code discord_to_minecraft.edited_message} pattern.
|
||||
* This is intentionally separated from {@code common.chat} so edit events can render a
|
||||
* "bottom bun" style complementary to {@code discord_to_minecraft.response}.
|
||||
*
|
||||
* @param message The edited Discord message.
|
||||
* @return The list of text segments for the edited message content line.
|
||||
*/
|
||||
public static List<TextSegment> buildEditedMessageSegments(Message message) {
|
||||
List<TextSegment> segments = new ArrayList<>();
|
||||
Member member = message.getMember();
|
||||
String effectiveName = member != null ? member.getEffectiveName() : message.getAuthor().getName();
|
||||
String roleColor = getRoleColorHex(member);
|
||||
String truncatedRaw = truncateMainRaw(message.getContentRaw());
|
||||
List<TextSegment> contentSegments = parseMessageContent(message, truncatedRaw);
|
||||
|
||||
JsonNode editedNode = I18nManager.getCustomMessages().path("discord_to_minecraft").path("edited_message");
|
||||
if (editedNode.isArray()) {
|
||||
for (JsonNode segNode : editedNode) {
|
||||
String text = segNode.path("text").asText("");
|
||||
boolean bold = segNode.path("bold").asBoolean(false);
|
||||
String color = segNode.path("color").asText("");
|
||||
|
||||
text = text.replace("{effective_name}", effectiveName);
|
||||
color = color.replace("{role_color}", roleColor);
|
||||
|
||||
if (text.contains("{message}")) {
|
||||
String[] parts = text.split("\\{message}", -1);
|
||||
if (!parts[0].isEmpty()) {
|
||||
segments.add(new TextSegment(parts[0], bold, color));
|
||||
}
|
||||
applyDefaultColor(contentSegments, color);
|
||||
segments.addAll(contentSegments);
|
||||
if (parts.length > 1 && !parts[1].isEmpty()) {
|
||||
segments.add(new TextSegment(parts[1], bold, color));
|
||||
}
|
||||
} else {
|
||||
segments.add(new TextSegment(text, bold, color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds segments for a message delete notification.
|
||||
* <p>
|
||||
|
|
@ -481,14 +529,17 @@ type = "image";
|
|||
} else if (attachment.isVideo()) {
|
||||
type = "video";
|
||||
}
|
||||
String label = "<attachment type=[" + type + "] name=[" + attachment.getFileName() + "]>";
|
||||
String label = "<attachment type=[" + type + "] name=[" + attachment.getFileName() + "]>";
|
||||
|
||||
TextSegment seg = new TextSegment(label, false, "#3366CC");
|
||||
seg.underlined = true;
|
||||
seg.clickUrl = attachment.getUrl();
|
||||
seg.hoverText = I18nManager.getDmccTranslation("discord.message_parser.click_to_open_link");
|
||||
segments.add(seg);
|
||||
}
|
||||
TextSegment seg = new TextSegment(label, false, "#3366CC");
|
||||
seg.underlined = true;
|
||||
seg.clickUrl = attachment.getUrl();
|
||||
seg.hoverText = I18nManager.getDmccTranslation("discord.message_parser.click_to_open_link");
|
||||
if (attachment.isSpoiler() || attachment.getFileName().startsWith("SPOILER_")) {
|
||||
seg.obfuscated = true;
|
||||
}
|
||||
segments.add(seg);
|
||||
}
|
||||
}
|
||||
|
||||
// Append stickers
|
||||
|
|
@ -516,16 +567,19 @@ title = safeTruncate(title, 20) + "...";
|
|||
}
|
||||
|
||||
TextSegment seg;
|
||||
if (embed.getUrl() == null) {
|
||||
seg = new TextSegment("<embed title=[" + title + "]>", false, "yellow");
|
||||
} else {
|
||||
seg = new TextSegment("<embed title=[" + title + "]>", false, "#3366CC");
|
||||
seg.underlined = true;
|
||||
seg.clickUrl = embed.getUrl();
|
||||
seg.hoverText = I18nManager.getDmccTranslation("discord.message_parser.click_to_open_link");
|
||||
}
|
||||
segments.add(seg);
|
||||
}
|
||||
if (embed.getUrl() == null) {
|
||||
seg = new TextSegment("<embed title=[" + title + "]>", false, "yellow");
|
||||
} else {
|
||||
seg = new TextSegment("<embed title=[" + title + "]>", false, "#3366CC");
|
||||
seg.underlined = true;
|
||||
seg.clickUrl = embed.getUrl();
|
||||
seg.hoverText = I18nManager.getDmccTranslation("discord.message_parser.click_to_open_link");
|
||||
}
|
||||
if (isSpoilerWrappedUrl(raw, embed.getUrl())) {
|
||||
seg.obfuscated = true;
|
||||
}
|
||||
segments.add(seg);
|
||||
}
|
||||
}
|
||||
|
||||
// Append interactive components indicator
|
||||
|
|
@ -1330,6 +1384,24 @@ lastEnd = span.end;
|
|||
return result;
|
||||
}
|
||||
|
||||
private static boolean isSpoilerWrappedUrl(String raw, String url) {
|
||||
if (raw == null || raw.isEmpty() || url == null || url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Matcher spoilerMatcher = SPOILER_CONTENT_PATTERN.matcher(raw);
|
||||
while (spoilerMatcher.find()) {
|
||||
String content = spoilerMatcher.group(1);
|
||||
if (content == null) {
|
||||
continue;
|
||||
}
|
||||
String normalized = content.replaceAll("[*_~`\\s]", "");
|
||||
if (url.equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String truncateMainRaw(String raw) {
|
||||
String lineLimited = applyMainLineLimit(raw);
|
||||
int maxLength = containsFullWidthCharacter(raw) ? MAIN_TRUNCATE_LIMIT_WIDE : MAIN_TRUNCATE_LIMIT_NARROW;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,16 @@ discord_to_minecraft:
|
|||
- text: "edited this message! Edited message becomes:"
|
||||
bold: false
|
||||
color: "gray"
|
||||
edited_message: # Edited message content format (reverse of response)
|
||||
- text: " <{effective_name}> "
|
||||
bold: false
|
||||
color: "{role_color}"
|
||||
- text: "└──── "
|
||||
bold: true
|
||||
color: "dark_gray"
|
||||
- text: "{message}"
|
||||
bold: false
|
||||
color: "dark_gray"
|
||||
delete: # Message delete notification format
|
||||
- text: "[Discord] "
|
||||
bold: true
|
||||
|
|
|
|||
|
|
@ -95,6 +95,16 @@ discord_to_minecraft:
|
|||
- text: "编辑了此消息!编辑后的消息为:"
|
||||
bold: false
|
||||
color: "gray"
|
||||
edited_message: # 编辑后的消息内容格式(与 response 反向)
|
||||
- text: " <{effective_name}> "
|
||||
bold: false
|
||||
color: "{role_color}"
|
||||
- text: "└──── "
|
||||
bold: true
|
||||
color: "dark_gray"
|
||||
- text: "{message}"
|
||||
bold: false
|
||||
color: "dark_gray"
|
||||
delete: # 消息删除通知格式
|
||||
- text: "[Discord] "
|
||||
bold: true
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue