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

@@ -230,7 +230,7 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
@Override
public void chat(String line) {
player.sendMessage(line);
player.sendMessage(LoginBook.chatLine(line));
}
@Override
@@ -4,14 +4,28 @@ import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import java.util.regex.Pattern;
/**
* LoginBook is the book the gate opens on join: the bind code, a line that opens the
* login page when clicked, and the reminder to use the system browser, where
* WebAuthn/passkey works (the WeChat/QQ in-app browsers break it).
* LoginBook renders what the gate shows a player: the book it opens on join (the bind
* code, a line that opens the login page when clicked, and the reminder to use the
* system browser, where WebAuthn/passkey works; the WeChat/QQ in-app browsers break
* it), and the chat lines, whose web addresses open when clicked.
*/
final class LoginBook {
// A web address in a chat line runs to the next space. The client makes nothing
// in server chat clickable by itself, so a player who closed the book had to type
// the login URL out by hand.
private static final LegacyComponentSerializer CHAT = LegacyComponentSerializer.builder()
.character(LegacyComponentSerializer.SECTION_CHAR)
.extractUrls(Pattern.compile("https?://\\S+"), Style.style(NamedTextColor.AQUA, TextDecoration.UNDERLINED))
.build();
private LoginBook() {
}
@@ -26,4 +40,9 @@ final class LoginBook {
.color(NamedTextColor.GRAY));
return Book.book(Component.text("Felis Login"), Component.text("Felis"), page);
}
/** chatLine renders one chat line with § colour codes, its web addresses clickable. */
static Component chatLine(String line) {
return CHAT.deserialize(line);
}
}
@@ -49,7 +49,7 @@ final class LoginFlow {
interface Seat {
String name();
/** chat sends one line, with § colour codes. */
/** chat sends one line, with § colour codes; a web address in it opens when clicked. */
void chat(String line);
/** showCode opens the login book. It may throw for a client that refuses it. */
@@ -14,6 +14,7 @@ import net.kyori.adventure.inventory.Book;
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.io.IOException;
import java.io.OutputStream;
@@ -119,6 +120,13 @@ public final class LoginFlowTest {
List.of(CODE + " " + stub.panelUrl), seat.books);
assertTrue("chat carries the code", seat.chat.get(0).contains(CODE));
assertTrue("chat names the panel URL", seat.chat.get(1).contains(stub.panelUrl) && seat.chat.get(2).contains(stub.panelUrl));
// The client makes nothing in server chat clickable by itself: a player who
// closed the book had to type the URL out.
for (int i = 1; i <= 2; i++) {
List<ClickEvent> clicks = new ArrayList<>();
collectClicks(LoginBook.chatLine(seat.chat.get(i)), clicks);
assertEq("chat line " + i + " opens the panel URL", List.of(ClickEvent.openUrl(stub.panelUrl)), clicks);
}
assertEq("no release before the link", 0, seat.releases.size());
int before = stub.hits("/link/status/");
@@ -357,6 +365,19 @@ public final class LoginFlowTest {
collectClicks(page, clicks);
assertEq("one click, opening the login page", List.of(ClickEvent.openUrl(CONSOLE + "/link")), clicks);
assertTrue("the page says to use the system browser", plain(page).contains("SYSTEM browser"));
// A chat line keeps its text; only the address in it turns into a link.
Component line = LoginBook.chatLine("§e[Felis] 打开 §b" + CONSOLE + "/link §e完成登录(勿用微信/QQ内置浏览器)。");
assertEq("chat line text", "[Felis] 打开 " + CONSOLE + "/link 完成登录(勿用微信/QQ内置浏览器)。", plain(line));
List<Component> links = new ArrayList<>();
collectLinks(line, links);
assertEq("one link in the line", 1, links.size());
assertEq("the link is the address alone", CONSOLE + "/link", plain(links.get(0)));
assertEq("the link opens it", ClickEvent.openUrl(CONSOLE + "/link"), links.get(0).clickEvent());
assertEq("the link looks like one", TextDecoration.State.TRUE, links.get(0).decoration(TextDecoration.UNDERLINED));
List<ClickEvent> none = new ArrayList<>();
collectClicks(LoginBook.chatLine(LoginFlow.SIGNED_IN), none);
assertEq("a line without an address has no link", List.of(), none);
}
// ---- fakes ----
@@ -642,6 +663,15 @@ public final class LoginFlowTest {
return sb.toString();
}
private static void collectLinks(Component c, List<Component> out) {
if (c.clickEvent() != null) {
out.add(c);
}
for (Component child : c.children()) {
collectLinks(child, out);
}
}
private static void collectClicks(Component c, List<ClickEvent> out) {
if (c.clickEvent() != null) {
out.add(c.clickEvent());