fix(limbo): 登录门聊天和 /link 回复里的网址可以直接点击打开

This commit is contained in:
Lemon-miaow committed 2026-09-27 15:13:08 +08:00
1 parent 1e42e9a87b
commit 9910c78709
8 files changed
+182 -22

No files matched your search

@@ -397,21 +397,8 @@ public final class FelisVelocityPlugin {
async(player, () -> {
try {
LinkCode code = linkClient.requestCode(player.getUniqueId());
player.sendMessage(Component.text(
zh ? "你的绑定码:" : "Your link code: ", NamedTextColor.GREEN)
.append(Component.text(code.code(), NamedTextColor.YELLOW)));
String panelUrl = code.panelUrl();
if (panelUrl != null) {
player.sendMessage(Component.text(
zh ? "在这里输入它完成绑定(几分钟内有效):"
: "Enter it here to finish linking (valid a few minutes):",
NamedTextColor.GRAY));
player.sendMessage(Component.text(" " + panelUrl, NamedTextColor.WHITE));
} else {
player.sendMessage(Component.text(
zh ? "在网页控制台 → 账户 中输入它完成绑定(几分钟内有效)。"
: "Enter it on the web console → Account to finish linking (valid a few minutes).",
NamedTextColor.GRAY));
for (Component line : LinkCodeCard.lines(code.code(), code.panelUrl(), zh)) {
player.sendMessage(line);
}
} catch (LinkException e) {
logger.warn("link code request failed for {} (status={}, code={}): {}",
@@ -0,0 +1,44 @@
package best.lolicon.felis.velocity;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import java.util.List;
/**
* LinkCodeCard builds the reply to {@code /link}: the bind code, then where to enter
* it. The panel address is a click-to-open link, because the client makes nothing in
* server chat clickable by itself and a bare address had to be typed out by hand.
*
* <p>Like {@link InviteCard} it holds no Velocity types, so {@link LinkCodeCardTest}
* can check the link without a live proxy. Sending is left to the caller.
*/
final class LinkCodeCard {
private LinkCodeCard() {
}
/** lines renders the reply; panelUrl is null when felis-api minted none. */
static List<Component> lines(String code, String panelUrl, boolean zh) {
Component codeLine = Component.text(zh ? "你的绑定码:" : "Your link code: ", NamedTextColor.GREEN)
.append(Component.text(code, NamedTextColor.YELLOW));
if (panelUrl == null) {
return List.of(codeLine, Component.text(
zh ? "在网页控制台 → 账户 中输入它完成绑定(几分钟内有效)。"
: "Enter it on the web console → Account to finish linking (valid a few minutes).",
NamedTextColor.GRAY));
}
Component link = Component.text(panelUrl, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
.clickEvent(ClickEvent.openUrl(panelUrl))
.hoverEvent(HoverEvent.showText(Component.text(
zh ? "点击在浏览器中打开" : "Click to open it in your browser")));
return List.of(codeLine,
Component.text(zh ? "在这里输入它完成绑定(几分钟内有效):"
: "Enter it here to finish linking (valid a few minutes):",
NamedTextColor.GRAY),
Component.text(" ").append(link));
}
}
@@ -0,0 +1,71 @@
package best.lolicon.felis.velocity;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.TextDecoration;
import java.util.List;
/**
* LinkCodeCardTest checks the {@code /link} reply: the code, and a panel address the
* player can click open instead of typing it out.
*
* <p>Hermetic and framework-free like {@link InviteCardTest}, and run the same way:
* {@code javac -cp <adventure-api.jar>:<adventure-key.jar>:<examination-api.jar> -d <out>
* velocity/src/main/java/best/lolicon/felis/velocity/LinkCodeCard.java
* velocity/test/best/lolicon/felis/velocity/LinkCodeCardTest.java && java -cp
* <out>:<adventure-api.jar>:<adventure-key.jar>:<examination-api.jar>
* best.lolicon.felis.velocity.LinkCodeCardTest}.
*/
public final class LinkCodeCardTest {
private static final String URL = "https://console.example.test/link";
private static int checks;
public static void main(String[] args) {
for (boolean zh : new boolean[] {true, false}) {
List<Component> card = LinkCodeCard.lines("K7P2QX", URL, zh);
String lang = zh ? "zh: " : "en: ";
assertEq(lang + "three lines", 3, card.size());
assertEq(lang + "the code line", zh ? "你的绑定码:K7P2QX" : "Your link code: K7P2QX", flat(card.get(0)));
assertEq(lang + "the address line", " " + URL, flat(card.get(2)));
List<Component> links = card.get(2).children();
assertEq(lang + "one link", 1, links.size());
Component link = links.get(0);
// Checked by equals alone: printing a ClickEvent needs examination-string,
// which this classpath leaves out, so a mismatch would die in toString.
assertTrue(lang + "the link opens the panel", ClickEvent.openUrl(URL).equals(link.clickEvent()));
assertEq(lang + "the link looks like one", TextDecoration.State.TRUE, link.decoration(TextDecoration.UNDERLINED));
assertTrue(lang + "the link says what a click does", link.hoverEvent() != null);
}
List<Component> bare = LinkCodeCard.lines("K7P2QX", null, true);
assertEq("no panel URL: two lines", 2, bare.size());
assertTrue("no panel URL: points at the console", flat(bare.get(1)).contains("网页控制台"));
assertTrue("no panel URL: nothing to click", bare.get(1).clickEvent() == null);
System.out.println("LinkCodeCardTest OK (" + checks + " checks)");
}
private static String flat(Component c) {
StringBuilder sb = new StringBuilder(c instanceof TextComponent ? ((TextComponent) c).content() : "");
for (Component child : c.children()) {
sb.append(flat(child));
}
return sb.toString();
}
private static void assertTrue(String what, boolean ok) {
if (!ok) {
throw new AssertionError(what);
}
checks++;
}
private static void assertEq(String what, Object want, Object got) {
if (want == null ? got != null : !want.equals(got)) {
throw new AssertionError(what + " = " + got + ", want " + want);
}
checks++;
}
}